Merge from the JDK6 branch.


git-svn-id: https://svn.apache.org/repos/asf/sis/trunk@1753294 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/core/sis-build-helper/src/main/ant/prepare-release.xml b/core/sis-build-helper/src/main/ant/prepare-release.xml
index 08e3028..221cb78 100644
--- a/core/sis-build-helper/src/main/ant/prepare-release.xml
+++ b/core/sis-build-helper/src/main/ant/prepare-release.xml
@@ -32,7 +32,7 @@
     <!-- Ensure that the "sis-build-helper" plugin used by the build is the released version. -->
     <replaceregexp file = "${user.dir}/pom.xml"
                   match = "&lt;sis\.plugin\.version&gt;.+&lt;/sis\.plugin\.version&gt;"
-                replace = "&lt;sis.plugin.version&gt;${sis.version}-SNAPSHOT&lt;/sis.plugin.version&gt;"/>
+                replace = "&lt;sis.plugin.version&gt;${branch.version}-SNAPSHOT&lt;/sis.plugin.version&gt;"/>
                 <!-- The -SNAPSHOT part will be removed later, at tag creation. -->
 
     <!-- Replace the version number in Java code. -->
@@ -44,9 +44,9 @@
     <replace dir="${user.dir}" failOnNoReplacements="true">
       <include name="**/pom.xml"/>
       <replacefilter token="svn.apache.org/repos/asf/sis/trunk"
-                     value="svn.apache.org/repos/asf/sis/branches/${sis.version}"/>
+                     value="svn.apache.org/repos/asf/sis/branches/${branch.version}"/>
       <replacefilter token="svn.apache.org/viewvc/sis/trunk"
-                     value="svn.apache.org/viewvc/sis/branches/${sis.version}"/>
+                     value="svn.apache.org/viewvc/sis/branches/${branch.version}"/>
     </replace>
   </target>
 
@@ -58,18 +58,18 @@
     <!-- Replace URL to branch by URL to the branch on Subversion. -->
     <replace dir="${user.dir}" failOnNoReplacements="true">
       <include name="**/pom.xml"/>
-      <replacefilter token="svn.apache.org/repos/asf/sis/branches/${sis.version}"
+      <replacefilter token="svn.apache.org/repos/asf/sis/branches/${branch.version}"
                      value="svn.apache.org/repos/asf/sis/tags/${sis.version}"/>
-      <replacefilter token="svn.apache.org/viewvc/sis/branches/${sis.version}"
+      <replacefilter token="svn.apache.org/viewvc/sis/branches/${branch.version}"
                      value="svn.apache.org/viewvc/sis/tags/${sis.version}"/>
     </replace>
 
     <!-- Replace version numbers. Note that no snapshot other than SIS can exist at this point. -->
     <replace dir="${user.dir}" failOnNoReplacements="true">
       <include name="**/pom.xml"/>
-      <replacefilter token="&lt;version&gt;${sis.version}-SNAPSHOT&lt;/version&gt;"
+      <replacefilter token="&lt;version&gt;${branch.version}-SNAPSHOT&lt;/version&gt;"
                      value="&lt;version&gt;${sis.version}&lt;/version&gt;"/>
-      <replacefilter token="&lt;sis.plugin.version&gt;${sis.version}-SNAPSHOT&lt;/sis.plugin.version&gt;"
+      <replacefilter token="&lt;sis.plugin.version&gt;${branch.version}-SNAPSHOT&lt;/sis.plugin.version&gt;"
                      value="&lt;sis.plugin.version&gt;${sis.version}&lt;/sis.plugin.version&gt;"/>
     </replace>
   </target>
diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/AbstractAssociation.java b/core/sis-feature/src/main/java/org/apache/sis/feature/AbstractAssociation.java
index 2694e6a..ed0635f 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/AbstractAssociation.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/AbstractAssociation.java
@@ -44,7 +44,8 @@
  * @version 0.6
  * @module
  *
- * @see DefaultAssociationRole#newInstance()
+ * @see AbstractFeature
+ * @see DefaultAssociationRole
  */
 public abstract class AbstractAssociation extends Field<AbstractFeature> implements Cloneable, Serializable {
     /**
diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/AbstractAttribute.java b/core/sis-feature/src/main/java/org/apache/sis/feature/AbstractAttribute.java
index 57c9560..975ce99 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/AbstractAttribute.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/AbstractAttribute.java
@@ -69,7 +69,8 @@
  * @version 0.6
  * @module
  *
- * @see DefaultAttributeType#newInstance()
+ * @see AbstractFeature
+ * @see DefaultAttributeType
  */
 public abstract class AbstractAttribute<V> extends Field<V> implements Cloneable, Serializable {
     /**
diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/AbstractIdentifiedType.java b/core/sis-feature/src/main/java/org/apache/sis/feature/AbstractIdentifiedType.java
index 81038b7..ded0b53 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/AbstractIdentifiedType.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/AbstractIdentifiedType.java
@@ -232,7 +232,7 @@
      * Returns a natural language designator for the element.
      * This can be used as an alternative to the {@linkplain #getName() name} in user interfaces.
      *
-     * @return Natural language designator for the element.
+     * @return Natural language designator for the element, or {@code null} if none.
      */
     public InternationalString getDesignation() {
         return designation;
diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/AbstractOperation.java b/core/sis-feature/src/main/java/org/apache/sis/feature/AbstractOperation.java
index e680d39..89a1b6b 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/AbstractOperation.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/AbstractOperation.java
@@ -25,6 +25,7 @@
 import org.opengis.parameter.ParameterDescriptorGroup;
 import org.opengis.parameter.ParameterValueGroup;
 import org.apache.sis.referencing.IdentifiedObjects;
+import org.apache.sis.util.Classes;
 import org.apache.sis.util.Debug;
 
 // Branch-dependent imports
@@ -51,8 +52,10 @@
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @since   0.6
- * @version 0.6
+ * @version 0.8
  * @module
+ *
+ * @see DefaultFeatureType
  */
 public abstract class AbstractOperation extends AbstractIdentifiedType {
     /**
@@ -78,7 +81,11 @@
 
     /**
      * Returns a map that can be used for creating the {@link #getResult()} type.
-     * This method can be invoked for subclass constructor.
+     * This method can be invoked for subclass constructor with the user-supplied map in argument.
+     * If the given map contains at least one key prefixed by {@value #RESULT_PREFIX}, then the values
+     * associated to those keys will be used.
+     *
+     * @param identification the map given by user to sub-class constructor.
      */
     final Map<String,Object> resultIdentification(final Map<String,?> identification) {
         final Map<String,Object> properties = new HashMap<String,Object>(6);
@@ -203,12 +210,12 @@
      * Returns a string representation of this operation.
      * The returned string is for debugging purpose and may change in any future SIS version.
      *
-     * @return A string representation of this operation for debugging purpose.
+     * @return a string representation of this operation for debugging purpose.
      */
     @Debug
     @Override
     public String toString() {
-        final StringBuilder buffer = new StringBuilder(40).append("Operation").append('[');
+        final StringBuilder buffer = new StringBuilder(40).append(Classes.getShortClassName(this)).append('[');
         final GenericName name = getName();
         if (name != null) {
             buffer.append('“');
@@ -222,13 +229,31 @@
             buffer.append(separator).append(IdentifiedObjects.toString(param.getName()));
             separator = ", ";
         }
-        if (separator == ", ") { // Identity comparaison is okay here.
+        if (separator == ", ") {                    // Identity comparaison is okay here.
             buffer.append(')');
         }
         final AbstractIdentifiedType result = getResult();
         if (result != null) {
-            buffer.append(" : ").append(result.getName());
+            final Object type;
+            if (result instanceof DefaultAttributeType<?>) {
+                type = Classes.getShortName(((DefaultAttributeType<?>) result).getValueClass());
+            } else {
+                type = result.getName();
+            }
+            buffer.append(" : ").append(type);
         }
-        return buffer.append(']').toString();
+        formatResultFormula(buffer.append(']'));
+        return buffer.toString();
+    }
+
+    /**
+     * Appends a string representation of the "formula" used for computing the result.
+     * The "formula" may be for example a link to another property.
+     *
+     * @param  buffer where to format the "formula".
+     * @return {@code true} if this method has formatted a formula, or {@code false} otherwise.
+     */
+    boolean formatResultFormula(Appendable buffer) {
+        return false;
     }
 }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultAssociationRole.java b/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultAssociationRole.java
index 1a4fc7e..8857973 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultAssociationRole.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultAssociationRole.java
@@ -50,6 +50,7 @@
  * @version 0.5
  * @module
  *
+ * @see DefaultFeatureType
  * @see AbstractAssociation
  */
 public class DefaultAssociationRole extends FieldType {
@@ -113,6 +114,8 @@
      * @param minimumOccurs  The minimum number of occurrences of the association within its containing entity.
      * @param maximumOccurs  The maximum number of occurrences of the association within its containing entity,
      *                       or {@link Integer#MAX_VALUE} if there is no restriction.
+     *
+     * @see org.apache.sis.feature.builder.AssociationRoleBuilder
      */
     public DefaultAssociationRole(final Map<String,?> identification, final DefaultFeatureType valueType,
             final int minimumOccurs, final int maximumOccurs)
diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultAttributeType.java b/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultAttributeType.java
index 29f1ce3..0a408bb 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultAttributeType.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultAttributeType.java
@@ -105,6 +105,7 @@
  * @version 0.5
  * @module
  *
+ * @see DefaultFeatureType
  * @see AbstractAttribute
  */
 public class DefaultAttributeType<V> extends FieldType {
@@ -179,6 +180,8 @@
      *                        For example if this new {@code DefaultAttributeType} describes a measurement,
      *                        then {@code characterizedBy} could holds the measurement accuracy.
      *                        See <cite>"Attribute characterization"</cite> in class Javadoc for more information.
+     *
+     * @see org.apache.sis.feature.builder.AttributeTypeBuilder
      */
     public DefaultAttributeType(final Map<String,?> identification, final Class<V> valueClass,
             final int minimumOccurs, final int maximumOccurs, final V defaultValue,
diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultFeatureType.java b/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultFeatureType.java
index c7b685a..996c16f 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultFeatureType.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultFeatureType.java
@@ -79,6 +79,12 @@
  * which are implicitly <cite>covariant</cite> (i.e. {@code String[]} can be casted to {@code CharSequence[]}, which
  * is safe for read operations but not for write operations — the later may throw {@link ArrayStoreException}).</div>
  *
+ * <div class="section">Instantiation</div>
+ * {@code DefaultFeatureType} can be instantiated directly by a call to its {@linkplain #DefaultFeatureType constructor}.
+ * But a more convenient approach may be to use the {@link org.apache.sis.feature.builder.FeatureTypeBuilder} instead,
+ * which provides shortcuts for frequently-used operations like creating various {@link org.opengis.util.GenericName}
+ * instances sharing the same namespace.
+ *
  * <div class="section">Immutability and thread safety</div>
  * Instances of this class are immutable if all properties ({@link GenericName} and {@link InternationalString}
  * instances) and all arguments ({@link AttributeType} instances) given to the constructor are also immutable.
@@ -90,6 +96,8 @@
  * @version 0.6
  * @module
  *
+ * @see DefaultAttributeType
+ * @see DefaultAssociationRole
  * @see AbstractFeature
  */
 public class DefaultFeatureType extends AbstractIdentifiedType implements FeatureType {
@@ -236,6 +244,8 @@
      * @param superTypes     The parents of this feature type, or {@code null} or empty if none.
      * @param properties     Any feature operation, any feature attribute type and any feature
      *                       association role that carries characteristics of a feature type.
+     *
+     * @see org.apache.sis.feature.builder.FeatureTypeBuilder
      */
     @SuppressWarnings("ThisEscapedInObjectConstruction")
     public DefaultFeatureType(final Map<String,?> identification, final boolean isAbstract,
diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/EnvelopeOperation.java b/core/sis-feature/src/main/java/org/apache/sis/feature/EnvelopeOperation.java
index cfc9334..b068b5c 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/EnvelopeOperation.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/EnvelopeOperation.java
@@ -136,7 +136,7 @@
                 final GenericName name = property.getName();
                 final String attributeName = (property instanceof LinkOperation)
                                              ? ((LinkOperation) property).referentName : name.toString();
-                final boolean isDefault = AttributeConvention.DEFAULT_GEOMETRY_PROPERTY.equals(name.tip());
+                final boolean isDefault = AttributeConvention.GEOMETRY_PROPERTY.equals(name.tip());
                 if (isDefault) {
                     defaultGeometry = attributeName;
                 }
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 68ab3d6..1d3f8b1 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;
@@ -202,7 +203,7 @@
         final StringBuffer  buffer  = new StringBuffer();
         final FieldPosition dummyFP = new FieldPosition(-1);
         for (final AbstractIdentifiedType propertyType : featureType.getProperties(true)) {
-            Object value;
+            Object value = null;
             if (feature != null) {
                 value = feature.getPropertyValue(propertyType.getName().toString());
                 if (value == null) {
@@ -212,8 +213,11 @@
                 }
             } else if (propertyType instanceof DefaultAttributeType<?>) {
                 value = ((DefaultAttributeType<?>) propertyType).getDefaultValue();
-            } else {
-                value = null;
+            } else if (propertyType instanceof AbstractOperation) {
+                if (((AbstractOperation) propertyType).formatResultFormula(buffer)) {
+                    value = CharSequences.trimWhitespaces(buffer).toString();
+                    buffer.setLength(0);
+                }
             }
             /*
              * Column 0 - Name.
@@ -223,31 +227,31 @@
             /*
              * Column 1 and 2 - Type and cardinality.
              */
-            final String   valueType;
-            final Class<?> valueClass;
-            final int minimumOccurs, maximumOccurs;
-            if (propertyType instanceof DefaultAttributeType<?>) {
-                final DefaultAttributeType<?> pt = (DefaultAttributeType<?>) propertyType;
+            final String   valueType;                       // The value to write in the type column.
+            final Class<?> valueClass;                      // AttributeType.getValueClass() if applicable.
+            final int minimumOccurs, maximumOccurs;         // Negative values mean no cardinality.
+            final AbstractIdentifiedType resultType;        // Result of operation if applicable.
+            if (propertyType instanceof AbstractOperation) {
+                resultType = ((AbstractOperation) propertyType).getResult();
+            } else {
+                resultType = propertyType;
+            }
+            if (resultType instanceof DefaultAttributeType<?>) {
+                final DefaultAttributeType<?> pt = (DefaultAttributeType<?>) resultType;
                 minimumOccurs = pt.getMinimumOccurs();
                 maximumOccurs = pt.getMaximumOccurs();
                 valueClass    = pt.getValueClass();
                 valueType     = getFormat(Class.class).format(valueClass, buffer, dummyFP).toString();
                 buffer.setLength(0);
-            } else if (propertyType instanceof DefaultAssociationRole) {
-                final DefaultAssociationRole pt = (DefaultAssociationRole) propertyType;
+            } else if (resultType instanceof DefaultAssociationRole) {
+                final DefaultAssociationRole pt = (DefaultAssociationRole) resultType;
                 minimumOccurs = pt.getMinimumOccurs();
                 maximumOccurs = pt.getMaximumOccurs();
                 valueType     = toString(DefaultAssociationRole.getValueTypeName(pt));
                 valueClass    = AbstractFeature.class;
-            } else if (propertyType instanceof AbstractOperation) {
-                final AbstractIdentifiedType resultType = ((AbstractOperation) propertyType).getResult();
-                valueType   = toString(resultType.getName());
-                valueClass  = null;
-                minimumOccurs = -1;
-                maximumOccurs = -1;
             } else {
-                valueType   = "";
-                valueClass  = null;
+                valueType  = toString(resultType.getName());
+                valueClass = null;
                 minimumOccurs = -1;
                 maximumOccurs = -1;
             }
@@ -305,7 +309,7 @@
                         Object c = attribute.getDefaultValue();
                         if (feature != null) {
                             final Object p = feature.getProperty(propertyType.getName().toString());
-                            if (p instanceof AbstractAttribute<?>) {    // Should always be true, but we are paranoiac.
+                            if (p instanceof AbstractAttribute<?>) {      // Should always be true, but we are paranoiac.
                                 c = ((AbstractAttribute<?>) p).characteristics().get(attribute.getName().toString());
                             }
                         }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/FeatureOperations.java b/core/sis-feature/src/main/java/org/apache/sis/feature/FeatureOperations.java
index b0e8c66..ed308d3 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/FeatureOperations.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/FeatureOperations.java
@@ -129,7 +129,7 @@
      * For example features of type <b>Country</b> may have identifiers named “ISO country code”
      * while features of type <b>Car</b> may have identifiers named “license plate number”.
      * In order to simplify identifier usages regardless of their name,
-     * an application could choose to add in all features a virtual property named {@code "@id"}
+     * an application could choose to add in all features a virtual property named {@code "identifier"}
      * which links to whatever property is used as an identifier in an arbitrary feature.
      * So the definition of the <b>Car</b> feature could contain the following code:
      *
@@ -137,7 +137,7 @@
      *   AttributeType licensePlateNumber = ...;            // Attribute creation omitted for brevity
      *   FeatureType car = new DefaultFeatureType(...,      // Arguments omitted for brevity
      *           licensePlateNumber, model, owner,
-     *           FeatureOperations.link(singletonMap(NAME_KEY, "@id"), licensePlateNumber);
+     *           FeatureOperations.link(singletonMap(NAME_KEY, "identifier"), licensePlateNumber);
      * }
      * </div>
      *
diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/LinkOperation.java b/core/sis-feature/src/main/java/org/apache/sis/feature/LinkOperation.java
index 0e08c75..d6e75f8 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/LinkOperation.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/LinkOperation.java
@@ -20,6 +20,7 @@
 import java.util.Map;
 import java.util.HashMap;
 import java.util.Collections;
+import java.io.IOException;
 import org.opengis.metadata.Identifier;
 import org.opengis.parameter.ParameterValueGroup;
 import org.opengis.parameter.ParameterDescriptor;
@@ -28,6 +29,9 @@
 import org.apache.sis.metadata.iso.citation.Citations;
 import org.apache.sis.util.ArgumentChecks;
 
+// Branch-dependent imports
+import org.apache.sis.internal.jdk8.UncheckedIOException;
+
 
 /**
  * A link operation, which is like a redirection or an alias.
@@ -145,4 +149,20 @@
         // 'this.result' is compared (indirectly) by the super class.
         return super.equals(obj) && referentName.equals(((LinkOperation) obj).referentName);
     }
+
+    /**
+     * Appends a string representation of the "formula" used for computing the result.
+     *
+     * @param  buffer where to format the "formula".
+     * @return {@code true} since this method has formatted a formula.
+     */
+    @Override
+    boolean formatResultFormula(final Appendable buffer) {
+        try {
+            buffer.append(" → ").append(referentName);
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+        return true;
+    }
 }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/StringJoinOperation.java b/core/sis-feature/src/main/java/org/apache/sis/feature/StringJoinOperation.java
index 7c17135..1767956 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/StringJoinOperation.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/StringJoinOperation.java
@@ -19,6 +19,7 @@
 import java.util.Arrays;
 import java.util.Map;
 import java.util.Set;
+import java.io.IOException;
 import org.opengis.parameter.ParameterDescriptorGroup;
 import org.opengis.parameter.ParameterValueGroup;
 import org.opengis.util.GenericName;
@@ -33,6 +34,7 @@
 
 // Branch-dependent imports
 import org.apache.sis.internal.jdk7.Objects;
+import org.apache.sis.internal.jdk8.UncheckedIOException;
 
 
 /**
@@ -414,4 +416,26 @@
         }
         return false;
     }
+
+    /**
+     * Appends a string representation of the "formula" used for computing the result.
+     *
+     * @param  buffer where to format the "formula".
+     * @return {@code true} since this method has formatted a formula.
+     */
+    @Override
+    boolean formatResultFormula(final Appendable buffer) {
+        try {
+            buffer.append(" → ");
+            String separator = "(";
+            for (final String element : attributeNames) {
+                buffer.append(separator).append(element);
+                separator = ", ";
+            }
+            buffer.append(')');
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+        return true;
+    }
 }
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
new file mode 100644
index 0000000..ad855a7
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/builder/AssociationRoleBuilder.java
@@ -0,0 +1,176 @@
+/*
+ * 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.feature.builder;
+
+import org.opengis.util.GenericName;
+import org.apache.sis.feature.DefaultAssociationRole;
+
+// Branch-dependent imports
+import org.apache.sis.feature.AbstractIdentifiedType;
+import org.apache.sis.feature.DefaultFeatureType;
+
+
+/**
+ * Describes one association from the {@code FeatureType} to be built by an {@code FeatureTypeBuilder} to another
+ * {@code FeatureType}. A different instance of {@code AssociationRoleBuilder} exists for each feature association
+ * to describe. Those instances are created preferably by {@link FeatureTypeBuilder#addAssociation(FeatureType)},
+ * or in case of cyclic reference by {@link FeatureTypeBuilder#addAssociation(GenericName)}.
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @since   0.8
+ * @version 0.8
+ * @module
+ *
+ * @see org.apache.sis.feature.DefaultAssociationRole
+ * @see FeatureTypeBuilder#addAssociation(FeatureType)
+ * @see FeatureTypeBuilder#addAssociation(GenericName)
+ */
+public final class AssociationRoleBuilder extends PropertyTypeBuilder {
+    /**
+     * The target feature type, or {@code null} if unknown.
+     */
+    private final DefaultFeatureType type;
+
+    /**
+     * Name of the target feature type (never null).
+     */
+    private final GenericName typeName;
+
+    /**
+     * Creates a new {@code AssociationRole} builder for values of the given type.
+     * The {@code type} argument can be null if unknown, but {@code typeName} is mandatory.
+     *
+     * @param owner  the builder of the {@code FeatureType} for which to add this property.
+     */
+    AssociationRoleBuilder(final FeatureTypeBuilder owner, final DefaultFeatureType type, final GenericName typeName) {
+        super(owner, null);
+        this.type     = type;
+        this.typeName = typeName;
+    }
+
+    /**
+     * Creates a new {@code FeatureAssociationRole} builder initialized to the values of an existing association.
+     *
+     * @param owner  the builder of the {@code FeatureType} for which to add this property.
+     */
+    AssociationRoleBuilder(final FeatureTypeBuilder owner, final DefaultAssociationRole template) {
+        super(owner, template);
+        minimumOccurs = template.getMinimumOccurs();
+        maximumOccurs = template.getMaximumOccurs();
+        type          = template.getValueType();
+        typeName      = type.getName();
+    }
+
+    /**
+     * Appends a text inside the value returned by {@link #toString()}, before the closing bracket.
+     */
+    @Override
+    final void toStringInternal(final StringBuilder buffer) {
+        buffer.append(" → ").append(typeName);
+    }
+
+    /**
+     * Returns a default name to use if the user did not specified a name. The first letter will be changed to
+     * lower case (unless the name looks like an acronym) for compliance with Java convention on property names.
+     */
+    @Override
+    final String getDefaultName() {
+        return typeName.tip().toString();
+    }
+
+    /**
+     * Sets the {@code FeatureAssociationRole} name as a generic name.
+     * If another name was defined before this method call, that previous value will be discarded.
+     *
+     * @return {@code this} for allowing method calls chaining.
+     */
+    @Override
+    public AssociationRoleBuilder setName(final GenericName name) {
+        super.setName(name);
+        return this;
+    }
+
+    /**
+     * Sets the {@code FeatureAssociationRole} name as a simple string with the default scope.
+     * The default scope is the value specified by the last call to
+     * {@link FeatureTypeBuilder#setDefaultScope(String)}.
+     * The name will be a {@linkplain org.apache.sis.util.iso.DefaultLocalName local name} if no default scope
+     * has been specified, or a {@linkplain org.apache.sis.util.iso.DefaultScopedName scoped name} otherwise.
+     *
+     * @return {@code this} for allowing method calls chaining.
+     */
+    @Override
+    public AssociationRoleBuilder setName(final String localPart) {
+        super.setName(localPart);
+        return this;
+    }
+
+    /**
+     * Sets the {@code FeatureAssociationRole} name as a string in the given scope.
+     * The name will be a {@linkplain org.apache.sis.util.iso.DefaultLocalName local name} if the given scope is
+     * {@code null} or empty, or a {@linkplain org.apache.sis.util.iso.DefaultScopedName scoped name} otherwise.
+     * If a {@linkplain FeatureTypeBuilder#setDefaultScope(String) default scope} has been specified, then the
+     * {@code scope} argument overrides it.
+     *
+     * @return {@code this} for allowing method calls chaining.
+     */
+    @Override
+    public AssociationRoleBuilder setName(final String scope, final String localPart) {
+        super.setName(scope, localPart);
+        return this;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public AssociationRoleBuilder setDefinition(final CharSequence definition) {
+        super.setDefinition(definition);
+        return this;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public AssociationRoleBuilder setDesignation(final CharSequence designation) {
+        super.setDesignation(designation);
+        return this;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public AssociationRoleBuilder setDescription(final CharSequence description) {
+        super.setDescription(description);
+        return this;
+    }
+
+    /**
+     * Creates a new property type from the current setting.
+     */
+    @Override
+    final AbstractIdentifiedType create() {
+        if (type != null) {
+            return new DefaultAssociationRole(identification(), type, minimumOccurs, maximumOccurs);
+        } else {
+            return new DefaultAssociationRole(identification(), typeName, minimumOccurs, maximumOccurs);
+        }
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/builder/AttributeRole.java b/core/sis-feature/src/main/java/org/apache/sis/feature/builder/AttributeRole.java
new file mode 100644
index 0000000..b9ed1a0
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/builder/AttributeRole.java
@@ -0,0 +1,70 @@
+/*
+ * 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.feature.builder;
+
+import org.apache.sis.feature.FeatureOperations;
+
+
+/**
+ * Roles that can be associated to some attributes for instructing {@code FeatureTypeBuilder}
+ * how to generate pre-defined operations. Those pre-defined operations are:
+ *
+ * <ul>
+ *   <li>A {@linkplain FeatureOperations#compound compound operation} for generating a unique identifier
+ *       from an arbitrary amount of attribute values.</li>
+ *   <li>A {@linkplain FeatureOperations#link link operation} for referencing a geometry to be used as the
+ *       <em>default</em> geometry.</li>
+ *   <li>An {@linkplain FeatureOperations#envelope operation} for computing the bounding box of all geometries
+ *       found in the feature. This operation is automatically added if the feature contains a default geometry.</li>
+ * </ul>
+ *
+ * This enumeration allows user code to specify which feature attribute to use for creating those operations.
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @since   0.8
+ * @version 0.8
+ * @module
+ *
+ * @see Attribute#addRole(AttributeRole)
+ */
+public enum AttributeRole {
+    /**
+     * Attribute value will be part of a unique identifier for the feature instance.
+     * An arbitrary amount of attributes can be flagged as identifier components:
+     *
+     * <ul>
+     *   <li>If no attribute has this role, then no attribute is marked as feature identifier.</li>
+     *   <li>If exactly one attribute has this role, then a synthetic attribute named {@code "@identifier"}
+     *       will be created as a {@linkplain FeatureOperations#link link} to the flagged attribute.</li>
+     *   <li>If more than one attribute have this role, then a synthetic attribute named {@code "@identifier"}
+     *       will be created as a {@linkplain FeatureOperations#compound compound key} made of all flagged
+     *       attributes. The separator character can be modified by a call to
+     *       {@link FeatureTypeBuilder#setIdentifierDelimiters(String, String, String)}</li>
+     * </ul>
+     *
+     * @see FeatureTypeBuilder#setIdentifierDelimiters(String, String, String)
+     */
+    IDENTIFIER_COMPONENT,
+
+    /**
+     * Attribute value will be flagged as the <em>default</em> geometry.
+     * Feature can have an arbitrary amount of geometry attributes,
+     * but only one can be flagged as the default geometry.
+     */
+    DEFAULT_GEOMETRY
+}
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
new file mode 100644
index 0000000..0a90c48
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/builder/AttributeTypeBuilder.java
@@ -0,0 +1,389 @@
+/*
+ * 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.feature.builder;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import org.opengis.util.GenericName;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.feature.DefaultAttributeType;
+import org.apache.sis.feature.FeatureOperations;
+import org.apache.sis.internal.util.CollectionsExt;
+import org.apache.sis.internal.feature.AttributeConvention;
+import org.apache.sis.internal.feature.Geometries;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.Classes;
+
+// Branch-dependent imports
+import java.util.Objects;
+import org.apache.sis.feature.AbstractIdentifiedType;
+
+
+/**
+ * Describes one attribute of the {@code FeatureType} to be built by the enclosing {@code FeatureTypeBuilder}.
+ * A different instance of {@code AttributeTypeBuilder} exists for each feature attribute to describe.
+ * Those instances are created by {@link FeatureTypeBuilder#addAttribute(Class)}.
+ *
+ * @param <V> the class of property values.
+ *
+ * @see org.apache.sis.feature.DefaultAttributeType
+ * @see FeatureTypeBuilder#addAttribute(Class)
+ */
+public final class AttributeTypeBuilder<V> extends PropertyTypeBuilder {
+    /**
+     * The class of property values. Can not be changed after construction
+     * because this value determines the parameterized type {@code <V>}.
+     */
+    private final Class<V> valueClass;
+
+    /**
+     * The default value for the property, or {@code null} if none.
+     */
+    private V defaultValue;
+
+    /**
+     * Whether this attribute will be used in a {@linkplain FeatureOperations#compound compound key} named
+     * {@code "@identifier"}. If only one attribute has this flag and {@link FeatureTypeBuilder#idPrefix} and
+     * {@code isSuffix} are null, then {@code "@identifier"} will be a {@linkplain FeatureOperations#link link}
+     * to {@code idAttributes[0]}.
+     *
+     * @see #addRole(AttributeRole)
+     */
+    private boolean isIdentifier;
+
+    /**
+     * Builders for the characteristics associated to the attribute.
+     */
+    private final List<CharacteristicTypeBuilder<?>> characteristics = new ArrayList<CharacteristicTypeBuilder<?>>();
+
+    /**
+     * Creates a new {@code AttributeType} builder for values of the given class.
+     *
+     * @param owner      the builder of the {@code FeatureType} for which to add this property.
+     * @param valueClass the class of property values.
+     */
+    AttributeTypeBuilder(final FeatureTypeBuilder owner, final Class<V> valueClass) {
+        super(owner, null);
+        this.valueClass = valueClass;
+    }
+
+    /**
+     * Creates a new {@code AttributeType} builder initialized to the values of an existing attribute.
+     *
+     * @param owner  the builder of the {@code FeatureType} for which to add this property.
+     */
+    AttributeTypeBuilder(final FeatureTypeBuilder owner, final DefaultAttributeType<V> template) {
+        super(owner, template);
+        minimumOccurs = template.getMinimumOccurs();
+        maximumOccurs = template.getMaximumOccurs();
+        valueClass    = template.getValueClass();
+        defaultValue  = template.getDefaultValue();
+        for (final DefaultAttributeType<?> c : template.characteristics().values()) {
+            characteristics.add(new CharacteristicTypeBuilder(this, c));
+        }
+    }
+
+    /**
+     * Returns a default name to use if the user did not specified a name. The first letter will be changed to
+     * lower case (unless the name looks like an acronym) for compliance with Java convention on property names.
+     */
+    @Override
+    final String getDefaultName() {
+        return Classes.getShortName(valueClass);
+    }
+
+    /**
+     * Sets the {@code AttributeType} name as a generic name.
+     * If another name was defined before this method call, that previous value will be discarded.
+     *
+     * @return {@code this} for allowing method calls chaining.
+     */
+    @Override
+    public AttributeTypeBuilder<V> setName(final GenericName name) {
+        super.setName(name);
+        return this;
+    }
+
+    /**
+     * Sets the {@code AttributeType} name as a simple string with the default scope.
+     * The default scope is the value specified by the last call to
+     * {@link FeatureTypeBuilder#setDefaultScope(String)}.
+     * The name will be a {@linkplain org.apache.sis.util.iso.DefaultLocalName local name} if no default scope
+     * has been specified, or a {@linkplain org.apache.sis.util.iso.DefaultScopedName scoped name} otherwise.
+     *
+     * @return {@code this} for allowing method calls chaining.
+     */
+    @Override
+    public AttributeTypeBuilder<V> setName(final String localPart) {
+        super.setName(localPart);
+        return this;
+    }
+
+    /**
+     * Sets the {@code AttributeType} name as a string in the given scope.
+     * The name will be a {@linkplain org.apache.sis.util.iso.DefaultLocalName local name} if the given scope is
+     * {@code null} or empty, or a {@linkplain org.apache.sis.util.iso.DefaultScopedName scoped name} otherwise.
+     * If a {@linkplain FeatureTypeBuilder#setDefaultScope(String) default scope} has been specified, then the
+     * {@code scope} argument overrides it.
+     *
+     * @return {@code this} for allowing method calls chaining.
+     */
+    @Override
+    public AttributeTypeBuilder<V> setName(final String scope, final String localPart) {
+        super.setName(scope, localPart);
+        return this;
+    }
+
+    /**
+     * Sets the default value for the property.
+     *
+     * @param  value  default property value, or {@code null} if none.
+     * @return {@code this} for allowing method calls chaining.
+     */
+    public AttributeTypeBuilder<V> setDefaultValue(final V value) {
+        if (!Objects.equals(defaultValue, value)) {
+            defaultValue = value;
+            clearCache();
+        }
+        return this;
+    }
+
+    /**
+     * Sets an enumeration of valid values for this attribute.
+     *
+     * <p>This is a convenience method for {@link #addCharacteristic(Class)} with a value
+     * of type {@link Set} and a conventional name.</p>
+     *
+     * @param  values valid values.
+     * @return {@code this} for allowing method calls chaining.
+     * @throws UnsupportedOperationException if this property does not support characteristics.
+     *
+     * @see #characteristics()
+     * @see AttributeConvention#VALID_VALUES_CHARACTERISTIC
+     */
+    @SafeVarargs
+    public final AttributeTypeBuilder<V> setValidValues(final V... values) {
+        return setCharacteristic(AttributeConvention.VALID_VALUES_CHARACTERISTIC,
+                Set.class, CollectionsExt.immutableSet(false, values));
+    }
+
+    /**
+     * Sets the maximal length that characterizes the {@link CharSequence} values of this attribute.
+     * While this characteristic can be applied to any kind of attribute, it is meaningful only with
+     * character sequences.
+     *
+     * <p>This is a convenience method for {@link #addCharacteristic(Class)} with a value
+     * of type {@link Integer} and a conventional name.</p>
+     *
+     * @param  length  maximal length of {@link CharSequence} attribute values, or {@code null}.
+     * @return {@code this} for allowing method calls chaining.
+     * @throws UnsupportedOperationException if this property does not support length characteristics.
+     *
+     * @see #characteristics()
+     * @see AttributeConvention#MAXIMAL_LENGTH_CHARACTERISTIC
+     */
+    public AttributeTypeBuilder<V> setMaximalLength(final Integer length) {
+        return setCharacteristic(AttributeConvention.MAXIMAL_LENGTH_CHARACTERISTIC, Integer.class, length);
+    }
+
+    /**
+     * Sets the coordinate reference system that characterizes the values of this attribute.
+     * While this characteristic can be applied to any kind of attribute, it is meaningful
+     * only with georeferenced values like geometries or coverages.
+     *
+     * <p>This is a convenience method for {@link #addCharacteristic(Class)} with a value
+     * of type {@link CoordinateReferenceSystem} and a conventional name.</p>
+     *
+     * @param  crs  coordinate reference system associated to attribute values, or {@code null}.
+     * @return {@code this} for allowing method calls chaining.
+     * @throws UnsupportedOperationException if this property does not support CRS characteristics.
+     *
+     * @see #characteristics()
+     * @see AttributeConvention#CRS_CHARACTERISTIC
+     */
+    public AttributeTypeBuilder<V> setCRS(final CoordinateReferenceSystem crs) {
+        return setCharacteristic(AttributeConvention.CRS_CHARACTERISTIC, CoordinateReferenceSystem.class, crs);
+    }
+
+    /**
+     * Implementation of all setter methods for characteristics.
+     */
+    private <C> AttributeTypeBuilder<V> setCharacteristic(final GenericName name, final Class<C> type, final C value) {
+        for (final CharacteristicTypeBuilder<?> characteristic : characteristics) {
+            if (name.equals(characteristic.getName())) {
+                characteristic.set(value);
+                return this;
+            }
+        }
+        addCharacteristic(type).setDefaultValue(value).setName(name);
+        return this;
+    }
+
+    /**
+     * Adds another attribute type that describes this attribute type.
+     * See <cite>"Attribute characterization"</cite> in {@link DefaultAttributeType} Javadoc for more information.
+     *
+     * <p>Usage example:</p>
+     * {@preformat java
+     *     attribute.addCharacteristic(Unit.class).setName("Unit of measurement").setDefaultValue(SI.CELSIUS);
+     * }
+     *
+     * The default characteristic name is the name of the given type, but callers should invoke one
+     * of the {@code CharacteristicTypeBuilder.setName(…)} methods on the returned instance with a better name.
+     *
+     * @param  <C>   the compile-time type of {@code type} argument.
+     * @param  type  the class of characteristic values.
+     * @return a builder for a characteristic of this attribute.
+     *
+     * @see #characteristics()
+     */
+    public <C> CharacteristicTypeBuilder<C> addCharacteristic(final Class<C> type) {
+        ensureNonNull("type", type);
+        final CharacteristicTypeBuilder<C> characteristic = new CharacteristicTypeBuilder<C>(this, type);
+        characteristics.add(characteristic);
+        clearCache();
+        return characteristic;
+    }
+
+    /**
+     * Adds another attribute type that describes this attribute type, using an existing one as a template.
+     * See <cite>"Attribute characterization"</cite> in {@link DefaultAttributeType} Javadoc for more information.
+     *
+     * <div class="warning"><b>Warning:</b>
+     * The {@code template} argument type will be changed to {@code AttributeType} if and when such interface
+     * will be defined in GeoAPI.</div>
+     *
+     * @param  <C>       the compile-time type of values in the {@code template} argument.
+     * @param  template  an existing attribute type to use as a template.
+     * @return a builder for a characteristic of this attribute, initialized with the values of the given template.
+     *
+     * @see #characteristics()
+     */
+    public <C> CharacteristicTypeBuilder<C> addCharacteristic(final DefaultAttributeType<C> template) {
+        ensureNonNull("template", template);
+        final CharacteristicTypeBuilder<C> characteristic = new CharacteristicTypeBuilder<C>(this, template);
+        characteristics.add(characteristic);
+        clearCache();
+        return characteristic;
+    }
+
+    /**
+     * Flags this attribute as an input of one of the pre-defined operations managed by {@code FeatureTypeBuilder}.
+     *
+     * @param role the role to add to this attribute (shall not be null).
+     */
+    public void addRole(final AttributeRole role) {
+        ensureNonNull("role", role);
+        switch (role) {
+            case IDENTIFIER_COMPONENT: {
+                if (!isIdentifier) {
+                    isIdentifier = true;
+                    owner.identifierCount++;
+                    owner.clearCache();         // The change does not impact this attribute itself.
+                }
+                break;
+            }
+            case DEFAULT_GEOMETRY: {
+                if (owner.defaultGeometry != this) {
+                    if (!Geometries.isKnownType(valueClass)) {
+                        throw new IllegalStateException(errors().getString(Errors.Keys.UnsupportedImplementation_1, valueClass));
+                    }
+                    if (owner.defaultGeometry != null) {
+                        throw new IllegalStateException(errors().getString(Errors.Keys.PropertyAlreadyExists_2,
+                                owner.getDisplayName(), AttributeConvention.GEOMETRY_PROPERTY));
+                    }
+                    owner.defaultGeometry = this;
+                    owner.clearCache();         // The change does not impact this attribute itself.
+                }
+                break;
+            }
+        }
+    }
+
+    /**
+     * Returns {@code true} if {@link AttributeRole#IDENTIFIER_COMPONENT} has been associated to this attribute.
+     */
+    @Override
+    boolean isIdentifier() {
+        return isIdentifier;
+    }
+
+    /**
+     * Returns a view of all characteristics added to the {@code AttributeType} to build.
+     * The returned list is <cite>live</cite>: changes in this builder are reflected in that list and conversely.
+     * However the returned list allows only {@linkplain List#remove(Object) remove} operations;
+     * new characteristics can be added only by calls to one of the {@code set/addCharacteristic(…)} methods.
+     *
+     * @return a live list over the characteristics declared to this builder.
+     *
+     * @see #addCharacteristic(Class)
+     * @see #addCharacteristic(AttributeType)
+     * @see #setValidValues(Object...)
+     * @see #setCRS(CoordinateReferenceSystem)
+     */
+    public List<CharacteristicTypeBuilder<?>> characteristics() {
+        return new RemoveOnlyList<CharacteristicTypeBuilder<?>>(characteristics);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public AttributeTypeBuilder<V> setDefinition(final CharSequence definition) {
+        super.setDefinition(definition);
+        return this;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public AttributeTypeBuilder<V> setDesignation(final CharSequence designation) {
+        super.setDesignation(designation);
+        return this;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public AttributeTypeBuilder<V> setDescription(final CharSequence description) {
+        super.setDescription(description);
+        return this;
+    }
+
+    /**
+     * Appends a text inside the value returned by {@link #toString()}, before the closing bracket.
+     */
+    @Override
+    final void toStringInternal(final StringBuilder buffer) {
+        buffer.append(" : ").append(Classes.getShortName(valueClass));
+    }
+
+    /**
+     * Creates a new property type from the current setting.
+     */
+    @Override
+    final AbstractIdentifiedType create() {
+        final DefaultAttributeType<?>[] chrts = new DefaultAttributeType<?>[characteristics.size()];
+        for (int i=0; i<chrts.length; i++) {
+            chrts[i] = characteristics.get(i).build();
+        }
+        return new DefaultAttributeType<V>(identification(), valueClass, minimumOccurs, maximumOccurs, defaultValue, chrts);
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/builder/CharacteristicTypeBuilder.java b/core/sis-feature/src/main/java/org/apache/sis/feature/builder/CharacteristicTypeBuilder.java
new file mode 100644
index 0000000..4eb2824
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/builder/CharacteristicTypeBuilder.java
@@ -0,0 +1,218 @@
+/*
+ * 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.feature.builder;
+
+import org.opengis.util.GenericName;
+import org.apache.sis.feature.DefaultAttributeType;
+import org.apache.sis.util.Classes;
+
+// Branch-dependent imports
+import java.util.Objects;
+
+
+/**
+ * Describes one characteristic of an {@code AttributeType} to be built by the enclosing {@code FeatureTypeBuilder}.
+ * A different instance of {@code CharacteristicTypeBuilder} exists for each characteristic to describe.
+ * Those instances are created by:
+ *
+ * <ul>
+ *   <li>{@link AttributeTypeBuilder#addCharacteristic(Class)}</li>
+ * </ul>
+ *
+ * @param <V> the class of characteristic values.
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @since   0.8
+ * @version 0.8
+ * @module
+ */
+public final class CharacteristicTypeBuilder<V> extends TypeBuilder {
+    /**
+     * The attribute type builder instance that created this {@code CharacteristicTypeBuilder} builder.
+     */
+    private final AttributeTypeBuilder<?> owner;
+
+    /**
+     * The class of attribute values. Can not be changed after construction
+     * because this value determines the parameterized type {@code <V>}.
+     */
+    private final Class<V> valueClass;
+
+    /**
+     * The default value for the attribute, or {@code null} if none.
+     */
+    private V defaultValue;
+
+    /**
+     * The characteristic created by this builder, or {@code null} if not yet created.
+     * This field must be cleared every time that a setter method is invoked on this builder.
+     */
+    private transient DefaultAttributeType<V> characteristic;
+
+    /**
+     * Creates a new characteristic builder for values of the given class.
+     *
+     * @param owner      the builder of the {@code AttributeType} for which to add this property.
+     * @param valueClass the class of characteristic values.
+     */
+    CharacteristicTypeBuilder(final AttributeTypeBuilder<?> owner, final Class<V> valueClass) {
+        super(null, owner.getLocale());
+        this.owner = owner;
+        this.valueClass = valueClass;
+    }
+
+    /**
+     * Creates a new characteristic builder initialized to the values of an existing attribute.
+     *
+     * @param owner  the builder of the {@code AttributeType} for which to add this property.
+     */
+    CharacteristicTypeBuilder(final AttributeTypeBuilder<?> owner, final DefaultAttributeType<V> template) {
+        super(template, owner.getLocale());
+        this.owner     = owner;
+        valueClass     = template.getValueClass();
+        defaultValue   = template.getDefaultValue();
+        characteristic = template;
+    }
+
+    /**
+     * If the {@code AttributeType<V>} created by the last call to {@link #build()} has been cached,
+     * clears that cache. This method must be invoked every time that a setter method is invoked.
+     */
+    @Override
+    final void clearCache() {
+        characteristic = null;
+        owner.clearCache();
+    }
+
+    /**
+     * Returns a default name to use if the user did not specified a name. The first letter will be changed to
+     * lower case (unless the name looks like an acronym) for compliance with Java convention on property names.
+     */
+    @Override
+    final String getDefaultName() {
+        return Classes.getShortName(valueClass);
+    }
+
+    /**
+     * Sets the characteristic name as a generic name.
+     * If another name was defined before this method call, that previous value will be discarded.
+     *
+     * @return {@code this} for allowing method calls chaining.
+     */
+    @Override
+    public CharacteristicTypeBuilder<V> setName(final GenericName name) {
+        super.setName(name);
+        return this;
+    }
+
+    /**
+     * Sets the characteristic name as a simple string with the default scope.
+     * The default scope is the value specified by the last call to
+     * {@link FeatureTypeBuilder#setDefaultScope(String)}.
+     * The name will be a {@linkplain org.apache.sis.util.iso.DefaultLocalName local name} if no default scope
+     * has been specified, or a {@linkplain org.apache.sis.util.iso.DefaultScopedName scoped name} otherwise.
+     *
+     * @return {@code this} for allowing method calls chaining.
+     */
+    @Override
+    public CharacteristicTypeBuilder<V> setName(final String localPart) {
+        super.setName(localPart);
+        return this;
+    }
+
+    /**
+     * Sets the characteristic name as a string in the given scope.
+     * The name will be a {@linkplain org.apache.sis.util.iso.DefaultLocalName local name} if the given scope is
+     * {@code null} or empty, or a {@linkplain org.apache.sis.util.iso.DefaultScopedName scoped name} otherwise.
+     * If a {@linkplain FeatureTypeBuilder#setDefaultScope(String) default scope} has been specified, then the
+     * {@code scope} argument overrides it.
+     *
+     * @return {@code this} for allowing method calls chaining.
+     */
+    @Override
+    public CharacteristicTypeBuilder<V> setName(final String scope, final String localPart) {
+        super.setName(scope, localPart);
+        return this;
+    }
+
+    /**
+     * Delegates the creation of a new name to the enclosing builder.
+     */
+    @Override
+    final GenericName name(final String scope, final String localPart) {
+        return owner.name(scope, localPart);
+    }
+
+    /**
+     * Sets the default value with check of the value class.
+     */
+    final void set(final Object value) {
+        setDefaultValue(valueClass.cast(value));
+    }
+
+    /**
+     * Sets the default value for the characteristic.
+     *
+     * @param  value  characteristic default value, or {@code null} if none.
+     * @return {@code this} for allowing method calls chaining.
+     */
+    public CharacteristicTypeBuilder<V> setDefaultValue(final V value) {
+        if (!Objects.equals(defaultValue, value)) {
+            defaultValue = value;
+            clearCache();
+        }
+        return this;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public CharacteristicTypeBuilder<V> setDefinition(final CharSequence definition) {
+        super.setDefinition(definition);
+        return this;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public CharacteristicTypeBuilder<V> setDesignation(final CharSequence designation) {
+        super.setDesignation(designation);
+        return this;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public CharacteristicTypeBuilder<V> setDescription(final CharSequence description) {
+        super.setDescription(description);
+        return this;
+    }
+
+    /**
+     * Creates a new characteristic from the current setting.
+     */
+    final DefaultAttributeType<V> build() {
+        if (characteristic == null) {
+            characteristic = new DefaultAttributeType<V>(identification(), valueClass, 0, 1, defaultValue);
+        }
+        return characteristic;
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/builder/FeatureTypeBuilder.java b/core/sis-feature/src/main/java/org/apache/sis/feature/builder/FeatureTypeBuilder.java
new file mode 100644
index 0000000..31b7f46
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/builder/FeatureTypeBuilder.java
@@ -0,0 +1,716 @@
+/*
+ * 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.feature.builder;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Locale;
+import org.opengis.util.GenericName;
+import org.opengis.util.NameFactory;
+import org.opengis.util.FactoryException;
+import org.apache.sis.feature.AbstractOperation;
+import org.apache.sis.feature.DefaultFeatureType;
+import org.apache.sis.feature.FeatureOperations;
+import org.apache.sis.internal.system.DefaultFactories;
+import org.apache.sis.internal.feature.AttributeConvention;
+import org.apache.sis.util.CorruptedObjectException;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.ArraysExt;
+
+// Branch-dependent imports
+import java.util.Objects;
+import org.apache.sis.feature.AbstractFeature;
+import org.apache.sis.feature.AbstractIdentifiedType;
+import org.apache.sis.feature.DefaultAssociationRole;
+import org.apache.sis.feature.DefaultAttributeType;
+
+
+/**
+ * Helper class for the creation of {@link FeatureType} instances.
+ * This builder can create the arguments to be given to the
+ * {@linkplain DefaultFeatureType#DefaultFeatureType feature type constructor}
+ * from simpler parameters given to this builder.
+ *
+ * <p>{@code FeatureTypeBuilder} should be short lived.
+ * After the {@code FeatureType} has been created, the builder should be discarded.</p>
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @since   0.8
+ * @version 0.8
+ * @module
+ *
+ * @see org.apache.sis.parameter.ParameterBuilder
+ */
+public class FeatureTypeBuilder extends TypeBuilder {
+    /**
+     * The factory to use for creating names.
+     */
+    private final NameFactory nameFactory;
+
+    /**
+     * Builders for the properties (attributes, associations or operations) of this feature.
+     */
+    private final List<PropertyTypeBuilder> properties;
+
+    /**
+     * The parent of the feature to create. By default, new features have no parent.
+     */
+    private final List<DefaultFeatureType> superTypes;
+
+    /**
+     * Whether the feature type is abstract. The default value is {@code false}.
+     *
+     * @see #isAbstract()
+     * @see #setAbstract(boolean)
+     */
+    private boolean isAbstract;
+
+    /**
+     * The default scope to use when {@link #name(String, String)} is invoked with a null scope.
+     *
+     * @see #getDefaultScope()
+     * @see #setDefaultScope(String)
+     */
+    private String defaultScope;
+
+    /**
+     * The default minimum number of property values.
+     *
+     * @see #setDefaultCardinality(int, int)
+     */
+    int defaultMinimumOccurs;
+
+    /**
+     * The default maximum number of property values.
+     *
+     * @see #setDefaultCardinality(int, int)
+     */
+    int defaultMaximumOccurs;
+
+    /**
+     * An optional prefix or suffix to insert before or after the {@linkplain FeatureOperations#compound compound key}
+     * named {@code "@identifier"}.
+     */
+    private String idPrefix, idSuffix;
+
+    /**
+     * The separator to insert between each single component in a {@linkplain FeatureOperations#compound compound key}
+     * named {@code "@identifier"}. This is ignored if {@link #identifierCount} is zero.
+     */
+    private String idDelimiter;
+
+    /**
+     * Number of attribute that have been flagged as an identifier component.
+     *
+     * @see AttributeRole#IDENTIFIER_COMPONENT
+     * @see AttributeConvention#IDENTIFIER_PROPERTY
+     */
+    int identifierCount;
+
+    /**
+     * The default geometry attribute, or {@code null} if none.
+     *
+     * @see AttributeRole#DEFAULT_GEOMETRY
+     * @see AttributeConvention#GEOMETRY_PROPERTY
+     */
+    AttributeTypeBuilder<?> defaultGeometry;
+
+    /**
+     * The object created by this builder, or {@code null} if not yet created.
+     * This field must be cleared every time that a setter method is invoked on this builder.
+     */
+    private transient DefaultFeatureType feature;
+
+    /**
+     * Creates a new builder instance using the default name factory.
+     */
+    public FeatureTypeBuilder() {
+        this(null, null, null);
+    }
+
+    /**
+     * Creates a new builder instance using the given feature type as a template.
+     *
+     * <div class="warning"><b>Warning:</b>
+     * The {@code template} argument type will be changed to {@code FeatureType} if and when such interface
+     * will be defined in GeoAPI.</div>
+     *
+     * @param template  an existing feature type to use as a template, or {@code null} if none.
+     */
+    public FeatureTypeBuilder(final DefaultFeatureType template) {
+        this(template, null, null);
+    }
+
+    /**
+     * Creates a new builder instance using the given name factory, template and locale for formatting error messages.
+     *
+     * <div class="warning"><b>Warning:</b>
+     * The {@code template} argument type will be changed to {@code FeatureType} if and when such interface
+     * will be defined in GeoAPI.</div>
+     *
+     * @param template  an existing feature type to use as a template, or {@code null} if none.
+     * @param factory   the factory to use for creating names, or {@code null} for the default factory.
+     * @param locale    the locale to use for formatting error messages, or {@code null} for the default locale.
+     */
+    public FeatureTypeBuilder(final DefaultFeatureType template, NameFactory factory, final Locale locale) {
+        super(template, locale);
+        if (factory == null) {
+            factory = DefaultFactories.forBuildin(NameFactory.class);
+        }
+        nameFactory = factory;
+        properties  = new ArrayList<PropertyTypeBuilder>();
+        superTypes  = new ArrayList<DefaultFeatureType>();
+        idDelimiter = ":";
+        defaultMinimumOccurs = 1;
+        defaultMaximumOccurs = 1;
+        if (template != null) {
+            feature    = template;
+            isAbstract = template.isAbstract();
+            superTypes.addAll(template.getSuperTypes());
+            for (final AbstractIdentifiedType p : template.getProperties(false)) {
+                final PropertyTypeBuilder builder;
+                if (p instanceof DefaultAttributeType<?>) {
+                    builder = new AttributeTypeBuilder(this, (DefaultAttributeType<?>) p);
+                } else if (p instanceof DefaultAssociationRole) {
+                    builder = new AssociationRoleBuilder(this, (DefaultAssociationRole) p);
+                } else {
+                    continue;           // Skip unknown types.
+                }
+                properties.add(builder);
+            }
+        }
+    }
+
+    /**
+     * If the {@code FeatureType} created by the last call to {@link #build()} has been cached,
+     * clears that cache. This method must be invoked every time that a setter method is invoked.
+     */
+    @Override
+    final void clearCache() {
+        feature = null;
+    }
+
+    /**
+     * Returns {@code true} if the feature type to create will act as an abstract super-type.
+     * Abstract types can not be {@linkplain DefaultFeatureType#newInstance() instantiated}.
+     *
+     * @return {@code true} if the feature type to create will act as an abstract super-type.
+     *
+     * @see DefaultFeatureType#isAbstract()
+     */
+    public boolean isAbstract() {
+        return isAbstract;
+    }
+
+    /**
+     * Sets whether the feature type to create will be abstract.
+     * If this method is not invoked, then the default value is {@code false}.
+     *
+     * @param  isAbstract whether the feature type will be abstract.
+     * @return {@code this} for allowing method calls chaining.
+     */
+    public FeatureTypeBuilder setAbstract(final boolean isAbstract) {
+        if (this.isAbstract != isAbstract) {
+            this.isAbstract  = isAbstract;
+            clearCache();
+        }
+        return this;
+    }
+
+    /**
+     * Returns the direct parents of the feature type to create.
+     *
+     * <div class="warning"><b>Warning:</b>
+     * The return type will be changed to {@code FeatureType[]} if and when such interface
+     * will be defined in GeoAPI.</div>
+     *
+     * @return the parents of the feature type to create, or an empty array if none.
+     *
+     * @see DefaultFeatureType#getSuperTypes()
+     */
+    public DefaultFeatureType[] getSuperTypes() {
+        return superTypes.toArray(new DefaultFeatureType[superTypes.size()]);
+    }
+
+    /**
+     * Sets the parent types (or super-type) from which to inherit properties.
+     * If this method is not invoked, then the default value is to have no parent.
+     *
+     * <div class="warning"><b>Warning:</b>
+     * The {@code parents} argument type will be changed to {@code FeatureType...} if and when such interface
+     * will be defined in GeoAPI.</div>
+     *
+     * @param  parents  the parent types from which to inherit properties, or an empty array if none.
+     * @return {@code this} for allowing method calls chaining.
+     */
+    public FeatureTypeBuilder setSuperTypes(final DefaultFeatureType... parents) {
+        ensureNonNull("parents", parents);
+        final List<DefaultFeatureType> asList = Arrays.asList(parents);
+        if (!superTypes.equals(asList)) {
+            superTypes.clear();
+            superTypes.addAll(asList);
+            clearCache();
+        }
+        return this;
+    }
+
+    /**
+     * Sets the {@code FeatureType} name as a generic name.
+     * If another name was defined before this method call, that previous value will be discarded.
+     *
+     * <div class="note"><b>Note for subclasses:</b>
+     * all {@code setName(…)} convenience methods in this builder delegate to this method.
+     * Consequently this method can be used as a central place where to control the creation of all names.</div>
+     *
+     * @return {@code this} for allowing method calls chaining.
+     */
+    @Override
+    public FeatureTypeBuilder setName(final GenericName name) {
+        super.setName(name);
+        return this;
+    }
+
+    /**
+     * Sets the {@code FeatureType} name as a simple string with the default scope.
+     * The default scope is the value specified by the last call to {@link #setDefaultScope(String)}.
+     * The name will be a {@linkplain org.apache.sis.util.iso.DefaultLocalName local name} if no default scope
+     * has been specified, or a {@linkplain org.apache.sis.util.iso.DefaultScopedName scoped name} otherwise.
+     *
+     * <p>This convenience method creates a {@link GenericName} instance,
+     * then delegates to {@link #setName(GenericName)}.</p>
+     *
+     * @return {@code this} for allowing method calls chaining.
+     */
+    @Override
+    public FeatureTypeBuilder setName(final String localPart) {
+        super.setName(localPart);
+        return this;
+    }
+
+    /**
+     * Sets the {@code FeatureType} name as a string in the given scope.
+     * The name will be a {@linkplain org.apache.sis.util.iso.DefaultLocalName local name} if the given scope is
+     * {@code null} or empty, or a {@linkplain org.apache.sis.util.iso.DefaultScopedName scoped name} otherwise.
+     * If a {@linkplain #setDefaultScope(String) default scope} has been specified, then the
+     * {@code scope} argument overrides it.
+     *
+     * <p>This convenience method creates a {@link GenericName} instance,
+     * then delegates to {@link #setName(GenericName)}.</p>
+     *
+     * @return {@code this} for allowing method calls chaining.
+     */
+    @Override
+    public FeatureTypeBuilder setName(final String scope, final String localPart) {
+        super.setName(scope, localPart);
+        return this;
+    }
+
+    /**
+     * Invoked by {@link TypeBuilder} for creating new {@code LocalName} or {@code GenericName} instances.
+     */
+    @Override
+    final GenericName name(String scope, final String localPart) {
+        if (scope == null) {
+            scope = getDefaultScope();
+        }
+        if (scope == null || scope.isEmpty()) {
+            return nameFactory.createLocalName(null, localPart);
+        } else {
+            return nameFactory.createGenericName(null, scope, localPart);
+        }
+    }
+
+    /**
+     * Returns the scope of the names created by {@code setName(String)} method calls.
+     *
+     * @return the scope to use by default when {@link #setName(String)} is invoked.
+     */
+    public String getDefaultScope() {
+        return defaultScope;
+    }
+
+    /**
+     * Sets the scope of the next names created by {@code setName(String)} method calls.
+     * This method applies only to the next calls to {@code setName(String)};
+     * the result of all previous calls stay unmodified.
+     *
+     * <p>There is different conventions about the use of name scopes. ISO 19109 suggests that the scope of all
+     * {@code AttributeType} names is the name of the enclosing {@code FeatureType}, but this is not mandatory.
+     * Users who want to apply this convention can invoke {@code setDefaultScope(featureName)} after
+     * <code>{@linkplain #setName(String) FeatureTypeBuilder.setName}(featureName)</code> but before
+     * <code>{@linkplain AttributeTypeBuilder#setName(String) AttributeTypeBuilder.setName}(attributeName)</code>.</p>
+     *
+     * @param  scope  the new default scope, or {@code null} if none.
+     * @return {@code this} for allowing method calls chaining.
+     */
+    public FeatureTypeBuilder setDefaultScope(final String scope) {
+        defaultScope = scope;
+        // No need to clear the cache because this change affects
+        // only the next names to be created, not the existing ones.
+        return this;
+    }
+
+    /**
+     * Sets the default minimum and maximum number of next attributes and associations to add.
+     * Those defaults will applied to newly created attributes or associations,
+     * for example in next calls to {@link #addAttribute(Class)}.
+     * Attributes and associations added before this method call are not modified.
+     *
+     * <p>If this method is not invoked, then the default cardinality is [1 … 1].</p>
+     *
+     * @param  minimumOccurs  new default minimum number of property values.
+     * @param  maximumOccurs  new default maximum number of property values.
+     * @return {@code this} for allowing method calls chaining.
+     *
+     * @see AttributeTypeBuilder#setCardinality(int, int)
+     */
+    public FeatureTypeBuilder setDefaultCardinality(final int minimumOccurs, final int maximumOccurs) {
+        if (minimumOccurs < 0 || maximumOccurs < minimumOccurs) {
+            throw new IllegalArgumentException(errors().getString(Errors.Keys.IllegalRange_2, minimumOccurs, maximumOccurs));
+        }
+        defaultMinimumOccurs = minimumOccurs;
+        defaultMaximumOccurs = maximumOccurs;
+        // No need to clear the cache because this change affects only
+        // the next properties to be created, not the existing ones.
+        return this;
+    }
+
+    /**
+     * Sets the prefix, suffix and delimiter to use when formatting a compound identifier made of two or more attributes.
+     * The delimiter will be used only if at least two attributes have the {@linkplain AttributeRole#IDENTIFIER_COMPONENT
+     * identifier component role}.
+     *
+     * <p>If this method is not invoked, then the default values are the {@code ":"} delimiter and no prefix or suffix.</p>
+     *
+     * @param  delimiter  the characters to use as delimiter between each single property value.
+     * @param  prefix     characters to use at the beginning of the concatenated string, or {@code null} if none.
+     * @param  suffix     characters to use at the end of the concatenated string, or {@code null} if none.
+     * @return {@code this} for allowing method calls chaining.
+     *
+     * @see AttributeRole#IDENTIFIER_COMPONENT
+     * @see FeatureOperations#compound(Map, String, String, String, PropertyType...)
+     */
+    public FeatureTypeBuilder setIdentifierDelimiters(final String delimiter, final String prefix, final String suffix) {
+        ensureNonEmpty("delimiter", delimiter);
+        if (!delimiter.equals(idDelimiter) || !Objects.equals(prefix, idPrefix) || !Objects.equals(suffix, idSuffix)) {
+            idDelimiter = delimiter;
+            idPrefix    = prefix;
+            idSuffix    = suffix;
+            clearCache();
+        }
+        return this;
+    }
+
+    /**
+     * Returns a view of all attributes and associations added to the {@code FeatureType} to build.
+     * The returned list is <cite>live</cite>: changes in this builder are reflected in that list and conversely.
+     * However the returned list allows only {@linkplain List#remove(Object) remove} operations;
+     * new attributes or associations can be added only by calls to one of the {@code addAttribute(…)}
+     * or {@code addAssociation(…)} methods.
+     *
+     * @return a live list over the properties declared to this builder.
+     *
+     * @see #addAttribute(Class)
+     * @see #addAttribute(AttributeType)
+     * @see #addAssociation(FeatureType)
+     * @see #addAssociation(GenericName)
+     * @see #addAssociation(FeatureAssociationRole)
+     */
+    public List<PropertyTypeBuilder> properties() {
+        return new RemoveOnlyList<PropertyTypeBuilder>(properties);
+    }
+
+    /**
+     * Creates a new {@code AttributeType} builder for values of the given class.
+     * The default attribute name is the name of the given type, but callers should invoke one
+     * of the {@code AttributeTypeBuilder.setName(…)} methods on the returned instance with a better name.
+     *
+     * <p>Usage example:</p>
+     * {@preformat java
+     *     builder.addAttribute(String.class).setName("City").setDefaultValue("Metropolis");
+     * }
+     *
+     * The value class can not be {@code Feature.class} since features shall be handled
+     * as {@linkplain #addAssociation(FeatureType) associations} instead than attributes.
+     *
+     * @param  <V>  the compile-time value of {@code valueClass} argument.
+     * @param  valueClass  the class of attribute values (can not be {@code Feature.class}).
+     * @return a builder for an {@code AttributeType}.
+     *
+     * @see #properties()
+     */
+    public <V> AttributeTypeBuilder<V> addAttribute(final Class<V> valueClass) {
+        ensureNonNull("valueClass", valueClass);
+        if (AbstractFeature.class.isAssignableFrom(valueClass)) {
+            // We disallow Feature.class because that type shall be handled as association instead than attribute.
+            throw new IllegalArgumentException(errors().getString(Errors.Keys.IllegalArgumentValue_2, "valueClass", valueClass));
+        }
+        final AttributeTypeBuilder<V> property = new AttributeTypeBuilder<V>(this, valueClass);
+        properties.add(property);
+        clearCache();
+        return property;
+    }
+
+    /**
+     * Creates a new {@code AttributeType} builder initialized to the same characteristics than the given template.
+     *
+     * <div class="warning"><b>Warning:</b>
+     * The {@code template} argument type will be changed to {@code AttributeType} if and when such interface
+     * will be defined in GeoAPI.</div>
+     *
+     * @param  <V>       the compile-time type of values in the {@code template} argument.
+     * @param  template  an existing attribute type to use as a template.
+     * @return a builder for an {@code AttributeType}, initialized with the values of the given template.
+     *
+     * @see #properties()
+     */
+    public <V> AttributeTypeBuilder<V> addAttribute(final DefaultAttributeType<V> template) {
+        ensureNonNull("template", template);
+        final AttributeTypeBuilder<V> property = new AttributeTypeBuilder<V>(this, template);
+        properties.add(property);
+        clearCache();
+        return property;
+    }
+
+    /**
+     * Creates a new {@code FeatureAssociationRole} builder for features of the given type.
+     * The default association name is the name of the given type, but callers should invoke one
+     * of the {@code AssociationRoleBuilder.setName(…)} methods on the returned instance with a better name.
+     *
+     * <div class="warning"><b>Warning:</b>
+     * The {@code type} argument type will be changed to {@code FeatureType} if and when such interface
+     * will be defined in GeoAPI.</div>
+     *
+     * @param  type  the type of feature values.
+     * @return a builder for a {@code FeatureAssociationRole}.
+     *
+     * @see #properties()
+     */
+    public AssociationRoleBuilder addAssociation(final DefaultFeatureType type) {
+        ensureNonNull("type", type);
+        final AssociationRoleBuilder property = new AssociationRoleBuilder(this, type, type.getName());
+        properties.add(property);
+        clearCache();
+        return property;
+    }
+
+    /**
+     * Creates a new {@code FeatureAssociationRole} builder for features of a type of the given name.
+     * This method can be invoked as an alternative to {@link #addAssociation(FeatureType)} when the
+     * {@code FeatureType} instance is not yet available because of cyclic dependency.
+     *
+     * @param  type  the name of the type of feature values.
+     * @return a builder for a {@code FeatureAssociationRole}.
+     *
+     * @see #properties()
+     */
+    public AssociationRoleBuilder addAssociation(final GenericName type) {
+        ensureNonNull("type", type);
+        final AssociationRoleBuilder property = new AssociationRoleBuilder(this, null, type);
+        properties.add(property);
+        clearCache();
+        return property;
+    }
+
+    /**
+     * Creates a new {@code FeatureAssociationRole} builder initialized to the same characteristics
+     * than the given template.
+     *
+     * <div class="warning"><b>Warning:</b>
+     * The {@code template} argument type will be changed to {@code FeatureAssociationRole} if and when such interface
+     * will be defined in GeoAPI.</div>
+     *
+     * @param  template  an existing feature association to use as a template.
+     * @return a builder for an {@code FeatureAssociationRole}, initialized with the values of the given template.
+     *
+     * @see #properties()
+     */
+    public AssociationRoleBuilder addAssociation(final DefaultAssociationRole template) {
+        ensureNonNull("template", template);
+        final AssociationRoleBuilder property = new AssociationRoleBuilder(this, template);
+        properties.add(property);
+        clearCache();
+        return property;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public FeatureTypeBuilder setDefinition(final CharSequence definition) {
+        super.setDefinition(definition);
+        return this;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public FeatureTypeBuilder setDesignation(final CharSequence designation) {
+        super.setDesignation(designation);
+        return this;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public FeatureTypeBuilder setDescription(final CharSequence description) {
+        super.setDescription(description);
+        return this;
+    }
+
+    /**
+     * Builds the feature type from the information and properties specified to this builder.
+     * One of the {@code setName(…)} methods must have been invoked before this {@code build()} method (mandatory).
+     * All other methods are optional, but some calls to a {@code add} method are usually needed.
+     *
+     * <div class="warning"><b>Warning:</b>
+     * The return type will be changed to {@code FeatureType} if and when such interface
+     * will be defined in GeoAPI.</div>
+     *
+     * @return the new feature type.
+     * @throws IllegalStateException if the feature type contains incompatible
+     *         {@linkplain AttributeTypeBuilder#setCRS CRS characteristics}.
+     */
+    public DefaultFeatureType build() throws IllegalStateException {
+        if (feature == null) {
+            /*
+             * Creates an initial array of property types with up to 3 slots reserved for @identifier, @geometry
+             * and @envelope operations. At first we presume that there is always an identifier.  The identifier
+             * slot will be removed later if there is none.
+             */
+            final int numSpecified = properties.size();     // Number of explicitely specified properties.
+            int numSynthetic;                               // Number of synthetic properties that may be generated.
+            int envelopeIndex = -1;
+            int geometryIndex = -1;
+            final AbstractIdentifiedType[] identifierTypes;
+            if (identifierCount == 0) {
+                numSynthetic    = 0;
+                identifierTypes = null;
+            } else {
+                numSynthetic    = 1;
+                identifierTypes = new AbstractIdentifiedType[identifierCount];
+            }
+            if (defaultGeometry != null) {
+                envelopeIndex = numSynthetic;
+                geometryIndex = numSynthetic + 1;
+                numSynthetic += 2;
+            }
+            final AbstractIdentifiedType[] propertyTypes = new AbstractIdentifiedType[numSynthetic + numSpecified];
+            int propertyCursor = numSynthetic;
+            int identifierCursor = 0;
+            for (int i=0; i<numSpecified; i++) {
+                final PropertyTypeBuilder builder = properties.get(i);
+                final AbstractIdentifiedType instance = builder.build();
+                propertyTypes[propertyCursor] = instance;
+                /*
+                 * Collect the attributes to use as identifier components while we loop over all properties.
+                 * A NullPointerException or an ArrayIndexOutOfBoundsException in this block would mean that
+                 * identifierCount field has not been updated correctly by an addRole(AttributeRole) method.
+                 */
+                if (builder.isIdentifier()) {
+                    identifierTypes[identifierCursor++] = instance;
+                }
+                /*
+                 * If there is a default geometry, add a link named "@geometry" to that geometry.
+                 * It may happen that the property created by the user is already named "@geometry",
+                 * in which case we will avoid to duplicate the property.
+                 */
+                if (builder == defaultGeometry) {
+                    if (propertyTypes[geometryIndex] != null) {
+                        // Assuming that there is no bug in our implementation, this error could happen if the user
+                        // has modified this FeatureTypeBuilder in another thread during this build() execution.
+                        throw new CorruptedObjectException();
+                    }
+                    if (AttributeConvention.GEOMETRY_PROPERTY.equals(instance.getName())) {
+                        System.arraycopy(propertyTypes, geometryIndex, propertyTypes, geometryIndex-1, (numSynthetic - geometryIndex) + i);
+                        geometryIndex = -1;
+                        numSynthetic--;
+                        continue;           // Skip the increment of propertyCursor.
+                    }
+                    propertyTypes[geometryIndex] = FeatureOperations.link(name(AttributeConvention.GEOMETRY_PROPERTY), instance);
+                }
+                propertyCursor++;
+            }
+            /*
+             * Create the "envelope" operation only after we created all other properties.
+             * Actually it is okay if the 'propertyTypes' array still contains null elements not needed for envelope calculation
+             * like "@identifier", since FeatureOperations.envelope(…) constructor ignores any property which is not for a value.
+             */
+            if (envelopeIndex >= 0) try {
+                propertyTypes[envelopeIndex] = FeatureOperations.envelope(name(AttributeConvention.ENVELOPE_PROPERTY), null, propertyTypes);
+            } catch (FactoryException e) {
+                throw new IllegalStateException(e);
+            }
+            /*
+             * If a synthetic identifier need to be created, create it now as the first property.
+             * It may happen that the user provided a single identifier component already named
+             * "@identifier", in which case we avoid to duplicate the property.
+             */
+            if (identifierTypes != null) {
+                if (identifierCursor != identifierTypes.length) {
+                    // Assuming that there is no bug in our implementation, this error could happen if the user
+                    // has modified this FeatureTypeBuilder in another thread during this build() execution.
+                    throw new CorruptedObjectException();
+                }
+                if (identifierCursor == 1 && AttributeConvention.IDENTIFIER_PROPERTY.equals(identifierTypes[0].getName())) {
+                    System.arraycopy(propertyTypes, 1, propertyTypes, 0, --propertyCursor);
+                } else {
+                    propertyTypes[0] = FeatureOperations.compound(name(AttributeConvention.IDENTIFIER_PROPERTY),
+                            idDelimiter, idPrefix, idSuffix, identifierTypes);
+                }
+            }
+            feature = new DefaultFeatureType(identification(), isAbstract(),
+                    superTypes.toArray(new DefaultFeatureType[superTypes.size()]),
+                    ArraysExt.resize(propertyTypes, propertyCursor));
+        }
+        return feature;
+    }
+
+    /**
+     * Helper method for creating identification info of synthetic attributes.
+     */
+    static Map<String,?> name(final GenericName name) {
+        return Collections.singletonMap(AbstractOperation.NAME_KEY, name);
+    }
+
+    /**
+     * Formats a string representation of this builder for debugging purpose.
+     */
+    @Override
+    final void toStringInternal(final StringBuilder buffer) {
+        if (isAbstract()) {
+            buffer.insert(buffer.indexOf("[") + 1, "abstract ");
+        }
+        String separator = " : ";
+        for (final DefaultFeatureType parent : superTypes) {
+            buffer.append(separator).append('“').append(parent.getName()).append('”');
+            separator = ", ";
+        }
+        buffer.append(" {");
+        separator = System.lineSeparator();
+        for (final PropertyTypeBuilder p : properties) {
+            p.toString(buffer.append(separator).append("    ").append(p.getClass().getSimpleName()));
+        }
+        buffer.append(separator).append('}');
+    }
+}
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
new file mode 100644
index 0000000..42bdfc6
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/builder/PropertyTypeBuilder.java
@@ -0,0 +1,151 @@
+/*
+ * 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.feature.builder;
+
+import org.opengis.util.GenericName;
+import org.apache.sis.util.resources.Errors;
+
+// Branch-dependent imports
+import org.apache.sis.feature.AbstractIdentifiedType;
+
+
+/**
+ * Describes one property of the {@code FeatureType} to be built by an {@code FeatureTypeBuilder}.
+ * A different instance of {@code PropertyTypeBuilder} exists for each property to describe.
+ * Those instances can be created by:
+ *
+ * <ul>
+ *   <li>{@link FeatureTypeBuilder#addAttribute(Class)}</li>
+ *   <li>{@link FeatureTypeBuilder#addAttribute(AttributeType)} for using an existing attribute as a template</li>
+ *   <li>{@link FeatureTypeBuilder#addAssociation(FeatureType)}</li>
+ *   <li>{@link FeatureTypeBuilder#addAssociation(GenericName)}</li>
+ *   <li>{@link FeatureTypeBuilder#addAssociation(FeatureAssociationRole)} for using an existing association as a template</li>
+ * </ul>
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @since   0.8
+ * @version 0.8
+ * @module
+ */
+public abstract class PropertyTypeBuilder extends TypeBuilder {
+    /**
+     * The feature type builder instance that created this {@code PropertyTypeBuilder}.
+     */
+    final FeatureTypeBuilder owner;
+
+    /**
+     * The minimum number of property values.
+     * The default value is 1, unless otherwise specified by {@link #setDefaultCardinality(int, int)}.
+     *
+     * @see #setCardinality(int, int)
+     */
+    int minimumOccurs;
+
+    /**
+     * The maximum number of property values.
+     * The default value is 1, unless otherwise specified by {@link #setDefaultCardinality(int, int)}.
+     *
+     * @see #setCardinality(int, int)
+     */
+    int maximumOccurs;
+
+    /**
+     * The attribute or association created by this builder, or {@code null} if not yet created.
+     * This field must be cleared every time that a setter method is invoked on this builder.
+     */
+    private transient AbstractIdentifiedType property;
+
+    /**
+     * Creates a new {@code PropertyType} builder initialized to the values of an existing property.
+     *
+     * @param owner     the builder of the {@code FeatureType} for which to add this property.
+     * @param template  an existing property to use as a template, or {@code null} if none.
+     */
+    PropertyTypeBuilder(final FeatureTypeBuilder owner, final AbstractIdentifiedType template) {
+        super(template, owner.getLocale());
+        this.owner    = owner;
+        minimumOccurs = owner.defaultMinimumOccurs;
+        maximumOccurs = owner.defaultMaximumOccurs;
+        property      = template;
+    }
+
+    /**
+     * Sets the minimum and maximum number of property values. Those numbers must be equal or greater than zero.
+     *
+     * <p>If this method is not invoked, then the default values are the cardinality specified by the last call
+     * to {@link FeatureTypeBuilder#setDefaultCardinality(int, int)} at the time this instance has been created.
+     * If the later method has not been invoked, then the default cardinality is [1 … 1].</p>
+     *
+     * @param  minimumOccurs  new minimum number of property values.
+     * @param  maximumOccurs  new maximum number of property values.
+     * @return {@code this} for allowing method calls chaining.
+     */
+    @SuppressWarnings("unchecked")
+    public PropertyTypeBuilder setCardinality(final int minimumOccurs, final int maximumOccurs) {
+        if (this.minimumOccurs != minimumOccurs || this.maximumOccurs != maximumOccurs) {
+            if (minimumOccurs < 0 || maximumOccurs < minimumOccurs) {
+                throw new IllegalArgumentException(errors().getString(Errors.Keys.IllegalRange_2, minimumOccurs, maximumOccurs));
+            }
+            this.minimumOccurs = minimumOccurs;
+            this.maximumOccurs = maximumOccurs;
+            clearCache();
+        }
+        return this;
+    }
+
+    /**
+     * Returns {@code true} if {@link AttributeRole#IDENTIFIER_COMPONENT} has been associated to this property.
+     */
+    boolean isIdentifier() {
+        return false;
+    }
+
+    /**
+     * Delegates the creation of a new name to the enclosing builder.
+     */
+    @Override
+    final GenericName name(final String scope, final String localPart) {
+        return owner.name(scope, localPart);
+    }
+
+    /**
+     * If the {@code PropertyType} created by the last call to {@link #build()} has been cached,
+     * clears that cache. This method must be invoked every time that a setter method is invoked.
+     */
+    @Override
+    final void clearCache() {
+        property = null;
+        owner.clearCache();
+    }
+
+    /**
+     * Returns the property type from the current setting.
+     * This method may return an existing property if it was already created.
+     */
+    final AbstractIdentifiedType build() {
+        if (property == null) {
+            property = create();
+        }
+        return property;
+    }
+
+    /**
+     * Creates a new property type from the current setting.
+     */
+    abstract AbstractIdentifiedType create();
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/builder/RemoveOnlyList.java b/core/sis-feature/src/main/java/org/apache/sis/feature/builder/RemoveOnlyList.java
new file mode 100644
index 0000000..327f7ed
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/builder/RemoveOnlyList.java
@@ -0,0 +1,51 @@
+/*
+ * 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.feature.builder;
+
+import java.util.AbstractList;
+import java.util.Iterator;
+import java.util.List;
+
+
+/**
+ * Wraps another list in a new list allowing only read and remove operations.
+ * Addition of new values are not allowed through this {@code RemoveOnlyList}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @since   0.8
+ * @version 0.8
+ * @module
+ */
+final class RemoveOnlyList<E> extends AbstractList<E> {
+    /**
+     * The original list to wrap.
+     */
+    private final List<E> elements;
+
+    /**
+     * Creates a new list wrapping the given list.
+     */
+    RemoveOnlyList(final List<E> elements) {
+        this.elements = elements;
+    }
+
+    @Override public void        clear()           {       elements.clear();}
+    @Override public int         size()            {return elements.size();}
+    @Override public E           get(int index)    {return elements.get(index);}
+    @Override public E           remove(int index) {return elements.get(index);}
+    @Override public Iterator<E> iterator()        {return elements.iterator();}
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/builder/TypeBuilder.java b/core/sis-feature/src/main/java/org/apache/sis/feature/builder/TypeBuilder.java
new file mode 100644
index 0000000..895a369
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/builder/TypeBuilder.java
@@ -0,0 +1,386 @@
+/*
+ * 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.feature.builder;
+
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Locale;
+import org.opengis.util.GenericName;
+import org.apache.sis.feature.AbstractIdentifiedType;
+import org.apache.sis.util.resources.Vocabulary;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.NullArgumentException;
+import org.apache.sis.util.Localized;
+import org.apache.sis.util.Classes;
+import org.apache.sis.util.Debug;
+
+// Branch-dependent imports
+import java.util.Objects;
+
+
+/**
+ * Properties common to all kind of types (feature, association, characteristics).
+ * Those properties are:
+ *
+ * <ul>
+ *   <li>the name        — a unique name which can be defined within a scope (or namespace).</li>
+ *   <li>the definition  — a concise definition of the element.</li>
+ *   <li>the designation — a natural language designator for the element for user interfaces.</li>
+ *   <li>the description — information beyond that required for concise definition of the element.</li>
+ * </ul>
+ *
+ * In many cases, the names of all {@code AttributeType}s and {@code AssociationRole}s to create
+ * within a {@code FeatureType} share the same namespace.
+ * For making name creations more convenient, a default namespace can be
+ * {@linkplain FeatureTypeBuilder#setDefaultScope specified once} and applied automatically
+ * to all names created by the {@link #setName(String)} method.
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @since   0.8
+ * @version 0.8
+ * @module
+ */
+public abstract class TypeBuilder implements Localized {
+    /**
+     * The feature name, definition, designation and description.
+     * The name is mandatory; all other information are optional.
+     */
+    private final Map<String,Object> identification = new HashMap<String,Object>(4);
+
+    /**
+     * Creates a new builder initialized to the values of an existing type.
+     */
+    TypeBuilder(final AbstractIdentifiedType template, final Locale locale) {
+        putIfNonNull(Errors.LOCALE_KEY, locale);
+        if (template != null) {
+            putIfNonNull(AbstractIdentifiedType.NAME_KEY,        template.getName());
+            putIfNonNull(AbstractIdentifiedType.DEFINITION_KEY,  template.getDefinition());
+            putIfNonNull(AbstractIdentifiedType.DESIGNATION_KEY, template.getDesignation());
+            putIfNonNull(AbstractIdentifiedType.DESCRIPTION_KEY, template.getDescription());
+        }
+    }
+
+    /**
+     * Puts the given value in the {@link #identification} map if the value is non-null.
+     * This method should be invoked only when the {@link #identification} map is known
+     * to not contain any value for the given key.
+     */
+    private void putIfNonNull(final String key, final Object value) {
+        if (value != null) {
+            identification.put(key, value);
+        }
+    }
+
+    /**
+     * Returns the map of properties to give to the {@code FeatureType} or {@code PropertyType} constructor.
+     * If the map does not contains a name, a default name may be generated.
+     */
+    @SuppressWarnings("ReturnOfCollectionOrArrayField")
+    final Map<String,Object> identification() {
+        if (identification.get(AbstractIdentifiedType.NAME_KEY) == null) {
+            String name = getDefaultName();
+            if (name != null) {
+                final int length = name.length();
+                if (length != 0) {
+                    final int c  = name.codePointAt(0);
+                    final int lc = Character.toLowerCase(c);
+                    if (c != lc) {
+                        final int n = Character.charCount(c);
+                        if (n >= length || Character.isLowerCase(name.codePointAt(n))) {
+                            final StringBuilder buffer = new StringBuilder(length);
+                            name = buffer.appendCodePoint(lc).append(name, n, length).toString();
+                        }
+                    }
+                    identification.put(AbstractIdentifiedType.NAME_KEY, name(null, name));
+                }
+            }
+        }
+        return identification;
+    }
+
+    /**
+     * If the object created by the last call to {@code build()} has been cached, clears that cache.
+     */
+    abstract void clearCache();
+
+    /**
+     * Creates a generic name from the given scope and local part.
+     * An empty scope means no scope. A {@code null} scope means the
+     * {@linkplain FeatureTypeBuilder#setDefaultScope(String) default scope}.
+     *
+     * @param scope      the scope of the name to create, or {@code null} if the name is local.
+     * @param localPart  the local part of the generic name (can not be {@code null}).
+     */
+    abstract GenericName name(String scope, String localPart);
+
+    /**
+     * Returns the name of the {@code IdentifiedType} to create, or {@code null} if undefined.
+     * This method returns the value built from the last call to a {@code setName(…)} method,
+     * or a default name or {@code null} if no name has been explicitely specified.
+     *
+     * @return the name of the {@code IdentifiedType} to create (may be a default name or {@code null}).
+     *
+     * @see AbstractIdentifiedType#getName()
+     */
+    public GenericName getName() {
+        return (GenericName) identification().get(AbstractIdentifiedType.NAME_KEY);
+    }
+
+    /**
+     * Returns a default name to use if the user did not specified a name. The first letter will be changed to
+     * lower case (unless the name looks like an acronym) for compliance with Java convention on property names.
+     */
+    String getDefaultName() {
+        return null;
+    }
+
+    /**
+     * Returns the name to use for displaying error messages.
+     */
+    final String getDisplayName() {
+        final GenericName name = getName();
+        return (name != null) ? name.toString() : Vocabulary.getResources(identification).getString(Vocabulary.Keys.Unnamed);
+    }
+
+    /**
+     * Sets the {@code IdentifiedType} name as a generic name.
+     * If another name was defined before this method call, that previous value will be discarded.
+     *
+     * <div class="note"><b>Note for subclasses:</b>
+     * all {@code setName(…)} convenience methods in this builder delegate to this method.
+     * Consequently this method can be used as a central place where to control the creation of all names.</div>
+     *
+     * @param  name  the generic name (can not be {@code null}).
+     * @return {@code this} for allowing method calls chaining.
+     *
+     * @see #getName()
+     * @see AbstractIdentifiedType#NAME_KEY
+     */
+    public TypeBuilder setName(final GenericName name) {
+        ensureNonNull("name", name);
+        if (!name.equals(identification.put(AbstractIdentifiedType.NAME_KEY, name))) {
+            clearCache();
+        }
+        return this;
+    }
+
+    /**
+     * Sets the {@code IdentifiedType} name as a simple string with the default scope.
+     * The default scope is the value specified by the last call to {@link FeatureTypeBuilder#setDefaultScope(String)}.
+     * The name will be a {@linkplain org.apache.sis.util.iso.DefaultLocalName local name} if no default scope
+     * has been specified, or a {@linkplain org.apache.sis.util.iso.DefaultScopedName scoped name} otherwise.
+     *
+     * <p>This convenience method creates a {@link GenericName} instance,
+     * then delegates to {@link #setName(GenericName)}.</p>
+     *
+     * @param  localPart  the local part of the generic name (can not be {@code null}).
+     * @return {@code this} for allowing method calls chaining.
+     *
+     * @see #getName()
+     */
+    public TypeBuilder setName(final String localPart) {
+        ensureNonEmpty("localPart", localPart);
+        return setName(name(null, localPart));
+    }
+
+    /**
+     * Sets the {@code IdentifiedType} name as a string in the given scope.
+     * The name will be a {@linkplain org.apache.sis.util.iso.DefaultLocalName local name} if the given scope is
+     * {@code null} or empty, or a {@linkplain org.apache.sis.util.iso.DefaultScopedName scoped name} otherwise.
+     * If a {@linkplain FeatureTypeBuilder#setDefaultScope(String) default scope} has been specified, then the
+     * {@code scope} argument overrides it.
+     *
+     * <p>This convenience method creates a {@link GenericName} instance,
+     * then delegates to {@link #setName(GenericName)}.</p>
+     *
+     * @param  scope      the scope of the name to create, or {@code null} if the name is local.
+     * @param  localPart  the local part of the generic name (can not be {@code null}).
+     * @return {@code this} for allowing method calls chaining.
+     *
+     * @see #getName()
+     */
+    public TypeBuilder setName(String scope, final String localPart) {
+        ensureNonEmpty("localPart", localPart);
+        if (scope == null) {
+            scope = "";                                 // For preventing the use of default scope.
+        }
+        return setName(name(scope, localPart));
+    }
+
+    /**
+     * Returns a concise definition of the element.
+     *
+     * @return concise definition of the element, or {@code null} if none.
+     *
+     * @see AbstractIdentifiedType#getDefinition()
+     */
+    public CharSequence getDefinition() {
+        return (CharSequence) identification.get(AbstractIdentifiedType.DEFINITION_KEY);
+    }
+
+    /**
+     * Sets a concise definition of the element.
+     *
+     * @param  definition a concise definition of the element, or {@code null} if none.
+     * @return {@code this} for allowing method calls chaining.
+     *
+     * @see #getDefinition()
+     * @see AbstractIdentifiedType#DEFINITION_KEY
+     */
+    public TypeBuilder setDefinition(final CharSequence definition) {
+        if (!Objects.equals(definition, identification.put(AbstractIdentifiedType.DEFINITION_KEY, definition))) {
+            clearCache();
+        }
+        return this;
+    }
+
+    /**
+     * Returns a natural language designator for the element.
+     * This can be used as an alternative to the {@linkplain #getName() name} in user interfaces.
+     *
+     * @return natural language designator for the element, or {@code null} if none.
+     *
+     * @see AbstractIdentifiedType#getDesignation()
+     */
+    public CharSequence getDesignation() {
+        return (CharSequence) identification.get(AbstractIdentifiedType.DESIGNATION_KEY);
+    }
+
+    /**
+     * Sets a natural language designator for the element.
+     * This can be used as an alternative to the {@linkplain #getName() name} in user interfaces.
+     *
+     * @param  designation a natural language designator for the element, or {@code null} if none.
+     * @return {@code this} for allowing method calls chaining.
+     *
+     * @see #getDesignation()
+     * @see AbstractIdentifiedType#DESIGNATION_KEY
+     */
+    public TypeBuilder setDesignation(final CharSequence designation) {
+        if (!Objects.equals(designation, identification.put(AbstractIdentifiedType.DESIGNATION_KEY, designation))) {
+            clearCache();
+        }
+        return this;
+    }
+
+    /**
+     * Returns optional information beyond that required for concise definition of the element.
+     * The description may assist in understanding the element scope and application.
+     *
+     * @return information beyond that required for concise definition of the element, or {@code null} if none.
+     *
+     * @see AbstractIdentifiedType#getDescription()
+     */
+    public CharSequence getDescription() {
+        return (CharSequence) identification.get(AbstractIdentifiedType.DESCRIPTION_KEY);
+    }
+
+    /**
+     * Sets optional information beyond that required for concise definition of the element.
+     * The description may assist in understanding the feature scope and application.
+     *
+     * @param  description  information beyond that required for concise definition of the element, or {@code null} if none.
+     * @return {@code this} for allowing method calls chaining.
+     *
+     * @see #getDescription()
+     * @see AbstractIdentifiedType#DESCRIPTION_KEY
+     */
+    public TypeBuilder setDescription(final CharSequence description) {
+        if (!Objects.equals(description, identification.put(AbstractIdentifiedType.DESCRIPTION_KEY, description))) {
+            clearCache();
+        }
+        return this;
+    }
+
+    /**
+     * Returns the locale used for formatting error messages, or {@code null} if unspecified.
+     * If unspecified, the system default locale will be used.
+     *
+     * @return the locale used for formatting error messages, or {@code null} if unspecified.
+     */
+    @Override
+    public Locale getLocale() {
+        return (Locale) identification.get(Errors.LOCALE_KEY);
+    }
+
+    /**
+     * Returns the resources for error messages.
+     */
+    final Errors errors() {
+        return Errors.getResources(identification);
+    }
+
+    /**
+     * Same as {@link org.apache.sis.util.ArgumentChecks#ensureNonNull(String, Object)},
+     * but uses the current locale in case of error.
+     *
+     * @param  name the name of the argument to be checked. Used only if an exception is thrown.
+     * @param  object the user argument to check against null value.
+     * @throws NullArgumentException if {@code object} is null.
+     */
+    final void ensureNonNull(final String name, final Object value) {
+        if (value == null) {
+            throw new NullArgumentException(errors().getString(Errors.Keys.NullArgument_1, name));
+        }
+    }
+
+    /**
+     * Same as {@link org.apache.sis.util.ArgumentChecks#ensureNonEmpty(String, CharSequence)},
+     * but uses the current locale in case of error.
+     *
+     * @param  name the name of the argument to be checked. Used only if an exception is thrown.
+     * @param  text the user argument to check against null value and empty sequences.
+     * @throws NullArgumentException if {@code text} is null.
+     * @throws IllegalArgumentException if {@code text} is empty.
+     */
+    final void ensureNonEmpty(final String name, final String text) {
+        if (text == null) {
+            throw new NullArgumentException(errors().getString(Errors.Keys.NullArgument_1, name));
+        }
+        if (text.length() == 0) {
+            throw new IllegalArgumentException(errors().getString(Errors.Keys.EmptyArgument_1, name));
+        }
+    }
+
+    /**
+     * Returns a string representation of this object.
+     * The returned string is for debugging purpose only and may change in any future SIS version.
+     *
+     * @return a string representation of this object for debugging purpose.
+     */
+    @Debug
+    @Override
+    public String toString() {
+        return toString(new StringBuilder(Classes.getShortClassName(this))).toString();
+    }
+
+    /**
+     * Partial implementation of {@link #toString()}. This method assumes that the class name
+     * has already been written in the buffer.
+     */
+    final StringBuilder toString(final StringBuilder buffer) {
+        toStringInternal(buffer.append("[“").append(getDisplayName()).append('”'));
+        return buffer.append(']');
+    }
+
+    /**
+     * Appends a text inside the value returned by {@link #toString()}, before the closing bracket.
+     */
+    void toStringInternal(StringBuilder buffer) {
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/builder/package-info.java b/core/sis-feature/src/main/java/org/apache/sis/feature/builder/package-info.java
new file mode 100644
index 0000000..58850f8
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/builder/package-info.java
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+/**
+ * Helper classes for creating {@code FeatureType} instances. Usage of this package is not mandatory,
+ * but make easier to create {@link org.apache.sis.feature.DefaultFeatureType} instances together with
+ * their attributes and associations.
+ *
+ * <p>The starting point is {@link org.apache.sis.feature.builder.FeatureTypeBuilder}.
+ * Example:</p>
+ *
+ * {@preformat java
+ *     // Create a feature type for a city, which contains a name and a population.
+ *     FeatureTypeBuilder builder = new FeatureTypeBuilder().setName("City");
+ *     builder.addAttribute(String.class).setName("name");
+ *     builder.addAttribute(Integer.class).setName("population");
+ *     FeatureType city = builder.build();
+ *
+ *     // Create a subclass for a city which is also a capital.
+ *     builder = new FeatureTypeBuilder().setName("Capital").setSuperTypes(city);
+ *     builder.addAttribute(String.class).setName("parliament");
+ *     FeatureType capital = builder.build();
+ * }
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @since   0.8
+ * @version 0.8
+ * @module
+ *
+ * @see org.apache.sis.feature.DefaultFeatureType
+ */
+package org.apache.sis.feature.builder;
diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/package-info.java b/core/sis-feature/src/main/java/org/apache/sis/feature/package-info.java
index 7506afd..40332cb 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/package-info.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/package-info.java
@@ -72,17 +72,29 @@
  * {@code      ├─} {@linkplain org.apache.sis.feature.DefaultAssociationRole  Feature association role}<br>
  * {@code      └─} {@linkplain org.apache.sis.feature.AbstractOperation       Operation}<br>
  * </td><td class="sep" style="width: 50%; white-space: nowrap">
- *             {@linkplain org.apache.sis.feature.AbstractFeature     Feature}             (<cite>sparse</cite> or <cite>dense</cite>)<br>
- *                                                                    Property<br>
- * {@code  ├─} {@linkplain org.apache.sis.feature.AbstractAttribute   Attribute}           (<cite>singleton</cite> or <cite>multi-valued</cite>)<br>
- * {@code  └─} {@linkplain org.apache.sis.feature.AbstractAssociation Feature association} (<cite>singleton</cite> or <cite>multi-valued</cite>)<br>
+ *                 Object<br>
+ * {@code  ├─}     {@linkplain org.apache.sis.feature.AbstractFeature     Feature}             (<cite>sparse</cite> or <cite>dense</cite>)<br>
+ * {@code  └─}                                                            Property<br>
+ * {@code      ├─} {@linkplain org.apache.sis.feature.AbstractAttribute   Attribute}           (<cite>singleton</cite> or <cite>multi-valued</cite>)<br>
+ * {@code      └─} {@linkplain org.apache.sis.feature.AbstractAssociation Feature association} (<cite>singleton</cite> or <cite>multi-valued</cite>)<br>
  * </td></tr></table>
  *
+ * <div class="section">Instantiation</div>
+ * Classes defined in this package are rarely instantiated directly (by a {@code new} statement).
+ * Instead those classes are instantiated indirectly by invoking a method on a parent container,
+ * or by using a builder. The starting point is {@code FeatureType}, which may be created by a
+ * {@link org.apache.sis.feature.builder.FeatureTypeBuilder} or may be provided by a
+ * {@link org.apache.sis.storage.DataStore} reading a data file.
+ * Once a {@code FeatureType} has been obtained, {@code Feature}s can be instantiated by calls to the
+ * {@link org.apache.sis.feature.DefaultFeatureType#newInstance() FeatureType.newInstance()} method.
+ * Once a {@code Feature} instance has been obtained, {@code Attribute}s can be instantiated indirectly
+ * by calls to the {@link org.apache.sis.feature.AbstractFeature#setPropertyValue Feature.setPropertyValue(…)} method.
+ *
  * @author  Travis L. Pinney
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  * @since   0.5
- * @version 0.5
+ * @version 0.8
  * @module
  */
 package org.apache.sis.feature;
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/AttributeConvention.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/AttributeConvention.java
index f6cb733..ce210d6 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/AttributeConvention.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/AttributeConvention.java
@@ -40,10 +40,10 @@
  * appropriate "real" property in the feature.
  *
  * <div class="note"><b>Example:</b>
- * one of the most frequently used synthetic property is {@code "@id"}, which contains a unique
+ * one of the most frequently used synthetic property is {@code "@identifier"}, which contains a unique
  * identifier (or primary key) for the feature. This property is usually (but not necessarily)
  * a {@linkplain org.apache.sis.feature.FeatureOperations#link link to an existing attribute}.
- * By using the {@code "@id"} alias, users do not need to know the name of the "real" attribute.
+ * By using the {@code "@identifier"} alias, users do not need to know the name of the "real" attribute.
  * </div>
  *
  * This class defines names for two kinds of usage:
@@ -60,7 +60,7 @@
  * but we may refactor this class in future SIS versions if there is a need to support different conventions.
  *
  * <p>In order to reduce the risk of name collision with properties in user-defined features
- * (e.g. the user may already have an attribute named {@code "id"} for his own purpose),
+ * (e.g. the user may already have an attribute named {@code "identifier"} for his own purpose),
  * all names defined in this class begin with the {@code "@"} character.</p>
  *
  * @author  Johann Sorel (Geomatys)
@@ -89,7 +89,7 @@
      * <p>The {@linkplain org.apache.sis.feature.DefaultAttributeType#getValueClass() value class} is usually
      * {@link String}, {@link Integer}, {@link java.util.UUID} or other types commonly used as identifiers.</p>
      */
-    public static final LocalName ID_PROPERTY;
+    public static final LocalName IDENTIFIER_PROPERTY;
 
     /**
      * Conventional name for a property containing the geometric object to use by default.
@@ -106,11 +106,11 @@
      *
      * @see #isGeometryAttribute(IdentifiedType)
      */
-    public static final LocalName DEFAULT_GEOMETRY_PROPERTY;
+    public static final LocalName GEOMETRY_PROPERTY;
 
     /**
      * Conventional name for fetching the envelope encompassing all geometries in a feature. Most {@code FeatureType}s
-     * have at most one geometry, which is also the {@linkplain #DEFAULT_GEOMETRY_PROPERTY default geometry}.
+     * have at most one geometry, which is also the {@link #GEOMETRY_PROPERTY default geometry}.
      * But if several geometries exist, then the value for this synthetic property is the union of all geometries.
      *
      * <p>Properties of this name are usually
@@ -125,10 +125,10 @@
      * Conventional name for fetching the Coordinate Reference System (CRS) of a geometry or a coverage.
      * This characteristic is typically an entry in the map returned by a call to the
      * {@link org.apache.sis.feature.DefaultAttributeType#characteristics()} method
-     * on the attribute referenced by {@link #DEFAULT_GEOMETRY_PROPERTY}.
+     * on the attribute referenced by {@link #GEOMETRY_PROPERTY}.
      *
      * <p>While it is technically possible to have different CRS for different feature instances,
-     * in most cases the CRS is the same for all geometries found in {@code DEFAULT_GEOMETRY_PROPERTY}.
+     * in most cases the CRS is the same for all geometries found in {@code GEOMETRY_PROPERTY}.
      * In such cases, the CRS can be specified only once as the
      * {@linkplain org.apache.sis.feature.DefaultAttributeType#getDefaultValue() default value}
      * of this {@code CRS_CHARACTERISTIC}.</p>
@@ -167,8 +167,8 @@
         final NameFactory factory = DefaultFactories.forBuildin(NameFactory.class);
         NAMESPACE                     = factory.createGenericName(null, "Apache", Constants.SIS);
         NameSpace ns                  = factory.createNameSpace(NAMESPACE, null);
-        ID_PROPERTY                   = factory.createLocalName(ns, "@identifier");
-        DEFAULT_GEOMETRY_PROPERTY     = factory.createLocalName(ns, "@geometry");
+        IDENTIFIER_PROPERTY           = factory.createLocalName(ns, "@identifier");
+        GEOMETRY_PROPERTY             = factory.createLocalName(ns, "@geometry");
         ENVELOPE_PROPERTY             = factory.createLocalName(ns, "@envelope");
         CRS_CHARACTERISTIC            = factory.createLocalName(ns, "@crs");
         MAXIMAL_LENGTH_CHARACTERISTIC = factory.createLocalName(ns, "@maximalLength");
@@ -222,7 +222,7 @@
      * @return {@code true} if the given type is (directly or indirectly) an attribute type
      *         for one of the recognized geometry types.
      *
-     * @see #DEFAULT_GEOMETRY_PROPERTY
+     * @see #GEOMETRY_PROPERTY
      */
     public static boolean isGeometryAttribute(AbstractIdentifiedType type) {
         while (type instanceof AbstractOperation) {
@@ -252,7 +252,7 @@
      * @throws ClassCastException if {@link #CRS_CHARACTERISTIC} has been found but is associated
      *         to an object which is not a {@link CoordinateReferenceSystem} instance.
      *
-     * @see org.apache.sis.internal.feature.FeatureTypeBuilder.Property#setCRSCharacteristic(CoordinateReferenceSystem)
+     * @see org.apache.sis.feature.builder.AttributeTypeBuilder#setCRS(CoordinateReferenceSystem)
      */
     public static CoordinateReferenceSystem getCRSCharacteristic(final Object attribute) {
         return (CoordinateReferenceSystem) getCharacteristic(attribute, CRS_CHARACTERISTIC.toString());
@@ -279,7 +279,7 @@
      * @throws ClassCastException if {@link #MAXIMAL_LENGTH_CHARACTERISTIC} has been found but is associated
      *         to an object which is not an {@link Integer} instance.
      *
-     * @see org.apache.sis.internal.feature.FeatureTypeBuilder.Property#setMaximalLengthCharacteristic(Integer)
+     * @see org.apache.sis.feature.builder.AttributeTypeBuilder#setMaximalLength(Integer)
      */
     public static Integer getMaximalLengthCharacteristic(final Object attribute) {
         return (Integer) getCharacteristic(attribute, MAXIMAL_LENGTH_CHARACTERISTIC.toString());
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Builder.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Builder.java
deleted file mode 100644
index 6f519e0..0000000
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Builder.java
+++ /dev/null
@@ -1,198 +0,0 @@
-/*
- * 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.feature;
-
-import java.util.HashMap;
-import java.util.Map;
-import org.opengis.util.GenericName;
-import org.apache.sis.feature.AbstractIdentifiedType;
-import org.apache.sis.util.resources.Vocabulary;
-import org.apache.sis.util.ArgumentChecks;
-
-
-/**
- * Base class of feature and attribute builders.
- * This base class provide the method needed for filling the {@code identification} map.
- *
- * @param <T> the builder subclass. It is subclass responsibility to ensure that {@code this}
- *            is assignable to {@code <T>}; this {@code Builder} class can not verify that.
- *
- * @author  Johann Sorel (Geomatys)
- * @author  Martin Desruisseaux (Geomatys)
- * @since   0.7
- * @version 0.7
- * @module
- */
-abstract class Builder<T extends Builder<T>> {
-    /**
-     * The feature name, definition, designation and description.
-     * The name is mandatory; all other information are optional.
-     */
-    final Map<String,Object> identification = new HashMap<String,Object>(4);
-
-    /**
-     * Creates a new builder instance.
-     */
-    Builder() {
-    }
-
-    /**
-     * Resets this builder to its initial state. After invocation of this method,
-     * this builder is in the same state than after construction.
-     *
-     * @return {@code this} for allowing method calls chaining.
-     */
-    @SuppressWarnings("unchecked")
-    public T clear() {
-        identification.clear();
-        return (T) this;
-    }
-
-    /**
-     * Creates a generic name from the given scope and local part.
-     * An empty scope means no scope. A {@code null} scope means the
-     * {@linkplain FeatureTypeBuilder#setDefaultScope(String) default scope}.
-     *
-     * @param scope      the scope of the name to create, or {@code null} if the name is local.
-     * @param localPart  the local part of the generic name (can not be {@code null}).
-     */
-    abstract GenericName name(String scope, String localPart);
-
-    /**
-     * Sets the feature type name as a simple string with the default scope.
-     * The default scope is the value specified by the last call to
-     * {@link FeatureTypeBuilder#setDefaultScope(String)}.
-     *
-     * <p>The name will be an instance of {@link org.opengis.util.LocalName} if no default scope
-     * has been specified, or an instance of {@link org.opengis.util.ScopedName} otherwise.</p>
-     *
-     * <p>This convenience method creates a {@link GenericName} instance,
-     * then delegates to {@link #setName(GenericName)}.</p>
-     *
-     * @param  localPart  the local part of the generic name (can not be {@code null}).
-     * @return {@code this} for allowing method calls chaining.
-     */
-    public T setName(String localPart) {
-        ArgumentChecks.ensureNonEmpty("localPart", localPart);
-        return setName(name(null, localPart));
-    }
-
-    /**
-     * Sets the feature type name as a string in the given scope.
-     * If a {@linkplain FeatureTypeBuilder#setDefaultScope(String) default scope} was specified,
-     * this method override it.
-     *
-     * <p>The name will be an instance of {@link org.opengis.util.LocalName} if the given scope
-     * is {@code null} or empty, or an instance of {@link org.opengis.util.ScopedName} otherwise.</p>
-     *
-     * <p>This convenience method creates a {@link GenericName} instance,
-     * then delegates to {@link #setName(GenericName)}.</p>
-     *
-     * @param  scope      the scope of the name to create, or {@code null} if the name is local.
-     * @param  localPart  the local part of the generic name (can not be {@code null}).
-     * @return {@code this} for allowing method calls chaining.
-     */
-    public T setName(String scope, String localPart) {
-        ArgumentChecks.ensureNonEmpty("localPart", localPart);
-        if (scope == null) {
-            scope = "";                                 // For preventing the use of default scope.
-        }
-        return setName(name(scope, localPart));
-    }
-
-    /**
-     * Sets the feature type name as a generic name.
-     * If another name was defined before this method call, that previous value will be discarded.
-     *
-     * <div class="note"><b>Note for subclasses:</b>
-     * all {@code setName(…)} convenience methods in this builder delegate to this method.
-     * Consequently this method can be used as a central place where to control the creation of all names.</div>
-     *
-     * @param  name  the generic name (can not be {@code null}).
-     * @return {@code this} for allowing method calls chaining.
-     *
-     * @see AbstractIdentifiedType#NAME_KEY
-     */
-    @SuppressWarnings("unchecked")
-    public T setName(GenericName name) {
-        ArgumentChecks.ensureNonNull("name", name);
-        identification.put(AbstractIdentifiedType.NAME_KEY, name);
-        return (T) this;
-    }
-
-    /**
-     * Returns the current {@code FeatureType} name, or {@code null} if undefined.
-     * This method returns the value built from the last call to a {@code setName(…)} method.
-     *
-     * @return the current {@code FeatureType} name, or {@code null} if the name has not yet been specified.
-     */
-    public GenericName getName() {
-        return (GenericName) identification.get(AbstractIdentifiedType.NAME_KEY);
-    }
-
-    /**
-     * Returns the name to use for displaying error messages.
-     */
-    final String getDisplayName() {
-        final GenericName name = getName();
-        return (name != null) ? name.toString() : Vocabulary.getResources(identification).getString(Vocabulary.Keys.Unnamed);
-    }
-
-    /**
-     * Sets a concise definition of the element.
-     *
-     * @param  definition a concise definition of the element, or {@code null} if none.
-     * @return {@code this} for allowing method calls chaining.
-     *
-     * @see AbstractIdentifiedType#DEFINITION_KEY
-     */
-    @SuppressWarnings("unchecked")
-    public T setDefinition(CharSequence definition) {
-        identification.put(AbstractIdentifiedType.DEFINITION_KEY, definition);
-        return (T) this;
-    }
-
-    /**
-     * Sets a natural language designator for the element.
-     * This can be used as an alternative to the {@linkplain #getName() name} in user interfaces.
-     *
-     * @param  designation a natural language designator for the element, or {@code null} if none.
-     * @return {@code this} for allowing method calls chaining.
-     *
-     * @see AbstractIdentifiedType#DESIGNATION_KEY
-     */
-    @SuppressWarnings("unchecked")
-    public T setDesignation(CharSequence designation) {
-        identification.put(AbstractIdentifiedType.DESIGNATION_KEY, designation);
-        return (T) this;
-    }
-
-    /**
-     * Sets optional information beyond that required for concise definition of the element.
-     * The description may assist in understanding the feature scope and application.
-     *
-     * @param  description  information beyond that required for concise definition of the element, or {@code null} if none.
-     * @return {@code this} for allowing method calls chaining.
-     *
-     * @see AbstractIdentifiedType#DESCRIPTION_KEY
-     */
-    @SuppressWarnings("unchecked")
-    public T setDescription(CharSequence description) {
-        identification.put(AbstractIdentifiedType.DESCRIPTION_KEY, description);
-        return (T) this;
-    }
-}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/FeatureTypeBuilder.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/FeatureTypeBuilder.java
deleted file mode 100644
index 2da4c2e..0000000
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/FeatureTypeBuilder.java
+++ /dev/null
@@ -1,765 +0,0 @@
-/*
- * 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.feature;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import org.opengis.util.GenericName;
-import org.opengis.util.NameFactory;
-import org.opengis.util.FactoryException;
-import org.opengis.referencing.crs.CoordinateReferenceSystem;
-import org.apache.sis.feature.AbstractOperation;
-import org.apache.sis.feature.DefaultAssociationRole;
-import org.apache.sis.feature.DefaultAttributeType;
-import org.apache.sis.feature.DefaultFeatureType;
-import org.apache.sis.feature.FeatureOperations;
-import org.apache.sis.internal.system.DefaultFactories;
-import org.apache.sis.internal.util.CollectionsExt;
-import org.apache.sis.util.NullArgumentException;
-import org.apache.sis.util.resources.Errors;
-import org.apache.sis.util.ArgumentChecks;
-import org.apache.sis.util.ArraysExt;
-
-// Branch-dependent imports
-import org.apache.sis.feature.AbstractFeature;
-import org.apache.sis.feature.AbstractIdentifiedType;
-
-
-/**
- * Helper class for the creation of {@link FeatureType} instances.
- * This builder can create the parameters to be given to the {@linkplain DefaultFeatureType#DefaultFeatureType
- * feature type constructor} from simpler parameters given to this builder.
- *
- * <p>{@code FeatureTypeBuilder} should be short lived.
- * After the {@code FeatureType} has been created, the builder should be discarded.</p>
- *
- * @author  Johann Sorel (Geomatys)
- * @author  Martin Desruisseaux (Geomatys)
- * @since   0.7
- * @version 0.7
- * @module
- *
- * @see org.apache.sis.parameter.ParameterBuilder
- */
-public class FeatureTypeBuilder extends Builder<FeatureTypeBuilder> {
-    /**
-     * The factory to use for creating names.
-     */
-    private final NameFactory nameFactory;
-
-    /**
-     * Builders for the properties of this feature.
-     */
-    private final List<Property<?>> properties;
-
-    /**
-     * The parent of the feature to create. By default, new features have no parent.
-     */
-    private final List<DefaultFeatureType> superTypes;
-
-    /**
-     * Whether the feature type is abstract. The default value is {@code false}.
-     */
-    private boolean isAbstract;
-
-    /**
-     * The default scope to use when {@link #name(String, String)} is invoked with a null scope.
-     *
-     * @see #setDefaultScope(String)
-     */
-    private String defaultScope;
-
-    /**
-     * The default minimum number of property values.
-     *
-     * @see #setDefaultCardinality(int, int)
-     */
-    private int defaultMinimumOccurs;
-
-    /**
-     * The default maximum number of property values.
-     *
-     * @see #setDefaultCardinality(int, int)
-     */
-    private int defaultMaximumOccurs;
-
-    /**
-     * If {@link #idAttributes} is non-null, an optional prefix or suffix to insert before
-     * or after the {@linkplain FeatureOperations#compound compound key} named {@code "@id"}.
-     */
-    private String idPrefix, idSuffix;
-
-    /**
-     * If {@link #idAttributes} is non-null and contains more than one value, the separator to insert between
-     * each single component in a {@linkplain FeatureOperations#compound compound key} named {@code "@id"}.
-     */
-    private String idDelimiter;
-
-    /**
-     * The attributes to use in a {@linkplain FeatureOperations#compound compound key} named {@code "@id"},
-     * or {@code null} if none. If this array contains only one property and {@link #idPrefix} is null,
-     * then {@code "@id"} will be a {@linkplain FeatureOperations#link link} to {@code idAttributes[0]}.
-     */
-    private final List<Property<?>> idAttributes;
-
-    /**
-     * The default geometry attribute, or {@code null} if none.
-     *
-     * @see AttributeConvention#DEFAULT_GEOMETRY_PROPERTY
-     */
-    private Property<?> defaultGeometry;
-
-    /**
-     * Creates a new builder instance using the default name factory.
-     */
-    public FeatureTypeBuilder() {
-        this(DefaultFactories.forBuildin(NameFactory.class));
-    }
-
-    /**
-     * Creates a new builder instance using the given name factory.
-     *
-     * @param factory  the factory to use for creating names.
-     */
-    public FeatureTypeBuilder(final NameFactory factory) {
-        nameFactory  = factory;
-        properties   = new ArrayList<Property<?>>();
-        superTypes   = new ArrayList<DefaultFeatureType>();
-        idAttributes = new ArrayList<Property<?>>();
-        idDelimiter  = ":";
-        defaultMinimumOccurs = 1;
-        defaultMaximumOccurs = 1;
-    }
-
-    /**
-     * Resets this builder to its initial state. After invocation of this method,
-     * this builder is in the same state than after construction.
-     *
-     * @return {@code this} for allowing method calls chaining.
-     */
-    @Override
-    public FeatureTypeBuilder clear() {
-        super.clear();
-        properties  .clear();
-        superTypes  .clear();
-        idAttributes.clear();
-        idDelimiter     = ":";
-        idPrefix        = null;
-        idSuffix        = null;
-        isAbstract      = false;
-        defaultGeometry = null;
-        defaultMinimumOccurs = 1;
-        defaultMaximumOccurs = 1;
-        return this;
-    }
-
-    /**
-     * Sets whether the feature type is abstract.
-     * If this method is not invoked, then the default value is {@code false}.
-     *
-     * @param  isAbstract whether the feature type is abstract.
-     * @return {@code this} for allowing method calls chaining.
-     */
-    public FeatureTypeBuilder setAbstract(final boolean isAbstract) {
-        this.isAbstract = isAbstract;
-        return this;
-    }
-
-    /**
-     * Sets the parent types (or super-type) from which to inherit properties.
-     * If this method is not invoked, then the default value is to have no parent.
-     *
-     * @param  parents  the parent types from which to inherit properties, or an empty array if none.
-     * @return {@code this} for allowing method calls chaining.
-     */
-    public FeatureTypeBuilder setSuperTypes(final DefaultFeatureType... parents) {
-        ArgumentChecks.ensureNonNull("parents", parents);
-        superTypes.clear();
-        superTypes.addAll(Arrays.asList(parents));
-        return this;
-    }
-
-    /**
-     * Sets the scope to use by default when {@link #setName(String)} is invoked.
-     *
-     * @param  scope  the new default scope, or {@code null} if none.
-     * @return {@code this} for allowing method calls chaining.
-     */
-    public FeatureTypeBuilder setDefaultScope(final String scope) {
-        defaultScope = scope;
-        return this;
-    }
-
-    /**
-     * Sets the default minimum and maximum number of property values.
-     * Those defaults will applied to newly created attributes or associations,
-     * for example in next calls to {@link #addAttribute(Class)}.
-     *
-     * <p>If this method is not invoked, then the default cardinality is [1 … 1].</p>
-     *
-     * @param  minimumOccurs  new default minimum number of property values.
-     * @param  maximumOccurs  new default maximum number of property values.
-     * @return {@code this} for allowing method calls chaining.
-     *
-     * @see Property#setCardinality(int, int)
-     */
-    public FeatureTypeBuilder setDefaultCardinality(final int minimumOccurs, final int maximumOccurs) {
-        if (minimumOccurs < 0 || maximumOccurs < minimumOccurs) {
-            throw new IllegalArgumentException(errors().getString(Errors.Keys.IllegalRange_2, minimumOccurs, maximumOccurs));
-        }
-        defaultMinimumOccurs = minimumOccurs;
-        defaultMaximumOccurs = maximumOccurs;
-        return this;
-    }
-
-    /**
-     * Sets the prefix, suffix and delimiter to use when formatting a compound identifier made of two or more attributes.
-     * The strings specified to this method will be used only if {@link #addIdentifier(Class)} is invoked more than once.
-     *
-     * <p>If this method is not invoked, then the default values are the {@code ":"} delimiter and no prefix or suffix.</p>
-     *
-     * @param  delimiter  the characters to use as delimiter between each single property value.
-     * @param  prefix     characters to use at the beginning of the concatenated string, or {@code null} if none.
-     * @param  suffix     characters to use at the end of the concatenated string, or {@code null} if none.
-     * @return {@code this} for allowing method calls chaining.
-     *
-     * @see java.util.StringJoiner
-     * @see FeatureOperations#compound(Map, String, String, String, PropertyType...)
-     */
-    public FeatureTypeBuilder setIdentifierDelimiters(final String delimiter, final String prefix, final String suffix) {
-        ArgumentChecks.ensureNonEmpty("delimiter", delimiter);
-        idDelimiter = delimiter;
-        idPrefix    = prefix;
-        idSuffix    = suffix;
-        return this;
-    }
-
-    /**
-     * Creates a new {@code AttributeType} builder for values of the given class which will be used as identifiers.
-     * An arbitrary amount of attributes can be specified as identifiers:
-     *
-     * <ul>
-     *   <li>If this method is never invoked, no attribute is marked as feature identifier.</li>
-     *   <li>If this method is invoked exactly once, then a new attribute is created in the same way than
-     *       {@link #addAttribute(Class)} and a synthetic attribute named {@code "@id"} will be created as
-     *       a {@linkplain FeatureOperations#link link} to the new attribute.</li>
-     *   <li>If this method is invoked more than once, then new attributes are created in the same way than
-     *       {@link #addAttribute(Class)} and a synthetic attribute named {@code "@id"} will be created as
-     *       a {@linkplain FeatureOperations#compound compound key} made of all identifiers.</li>
-     * </ul>
-     *
-     * Callers shall invoke at least one of the {@code Property.setName(…)} methods on the returned instance.
-     * All other methods are optional.
-     *
-     * @param  <V>  the compile-time value of {@code valueClass} argument.
-     * @param  valueClass  the class of attribute values.
-     * @return a builder for an {@code AttributeType}.
-     */
-    public <V> Property<V> addIdentifier(final Class<V> valueClass) {
-        ensureAttributeType(valueClass);
-        final Property<V> property = new Property<V>(valueClass);
-        idAttributes.add(property);
-        properties.add(property);
-        return property;
-    }
-
-    /**
-     * Creates a new {@code AttributeType} builder for a geometry which will be flagged as the default geometry.
-     * Callers shall invoke at least one of the {@code Property.setName(…)} methods on the returned instance.
-     * All other methods are optional.
-     *
-     * @param  <V>  the compile-time value of {@code valueClass} argument.
-     * @param  valueClass  the geometry class of attribute values.
-     * @return a builder for an {@code AttributeType} which will contain the default geometry.
-     * @throws IllegalArgumentException if the given type is not a supported geometry type.
-     * @throws IllegalStateException if a default geometry has already been specified to this builder.
-     */
-    public <V> Property<V> addDefaultGeometry(final Class<V> valueClass) {
-        ensureAttributeType(valueClass);
-        if (!Geometries.isKnownType(valueClass)) {
-            throw new IllegalArgumentException(errors().getString(Errors.Keys.UnsupportedImplementation_1, valueClass));
-        }
-        if (defaultGeometry != null) {
-            throw new IllegalStateException(errors().getString(Errors.Keys.PropertyAlreadyExists_2,
-                    getDisplayName(), AttributeConvention.DEFAULT_GEOMETRY_PROPERTY));
-        }
-        final Property<V> property = new Property<V>(valueClass);
-        defaultGeometry = property;
-        properties.add(property);
-        return property;
-    }
-
-    /**
-     * Creates a new {@code AttributeType} builder for values of the given class.
-     * Callers shall invoke at least one of the {@code Property.setName(…)} methods on the returned instance.
-     * All other methods are optional.
-     *
-     * <p>Usage example:</p>
-     * {@preformat java
-     *     builder.addAttribute(String.class).setName("City");
-     * }
-     *
-     * @param  <V>  the compile-time value of {@code valueClass} argument.
-     * @param  valueClass  the class of attribute values.
-     * @return a builder for an {@code AttributeType}.
-     */
-    public <V> Property<V> addAttribute(final Class<V> valueClass) {
-        ensureAttributeType(valueClass);
-        final Property<V> property = new Property<V>(valueClass);
-        properties.add(property);
-        return property;
-    }
-
-    /**
-     * Ensures that the given value class is not null and not assignable to {@code Feature}.
-     * We disallow {@code Feature.class} because those type shall be handled as associations
-     * instead than attributes.
-     */
-    private void ensureAttributeType(final Class<?> valueClass) {
-        if (valueClass == null) {
-            throw new NullArgumentException(errors().getString(Errors.Keys.NullArgument_1, "valueClass"));
-        }
-        if (AbstractFeature.class.isAssignableFrom(valueClass)) {
-            throw new IllegalArgumentException(errors().getString(Errors.Keys.IllegalArgumentValue_2, "valueClass", valueClass));
-        }
-    }
-
-    /**
-     * Creates a new {@code FeatureAssociationRole} builder for features of the given type.
-     * Callers shall invoke at least one of the {@code Property.setName(…)} methods on the returned instance.
-     * All other methods are optional.
-     *
-     * @param  type  the type of feature values.
-     * @return a builder for a {@code FeatureAssociationRole}.
-     */
-    public Property<AbstractFeature> addAssociation(final DefaultFeatureType type) {
-        ArgumentChecks.ensureNonNull("type", type);
-        final Property<AbstractFeature> property = new Property<AbstractFeature>(AbstractFeature.class, DefaultFeatureType.class, type);
-        properties.add(property);
-        return property;
-    }
-
-    /**
-     * Creates a new {@code FeatureAssociationRole} builder for features of a type of the given name.
-     * This method can be invoked as an alternative to {@link #addAssociation(FeatureType)} when the
-     * {@code FeatureType} instance is not yet available because of cyclic dependency.
-     *
-     * @param  type  the name of the type of feature values.
-     * @return a builder for a {@code FeatureAssociationRole}.
-     */
-    public Property<AbstractFeature> addAssociation(final GenericName type) {
-        ArgumentChecks.ensureNonNull("type", type);
-        final Property<AbstractFeature> property = new Property<AbstractFeature>(AbstractFeature.class, GenericName.class, type);
-        properties.add(property);
-        return property;
-    }
-
-    /**
-     * Describes one property of the {@code FeatureType} to be built by the enclosing {@code FeatureTypeBuilder}.
-     * A different instance of {@code Property} exists for each property to describe. Those instances are created by:
-     *
-     * <ul>
-     *   <li>{@link FeatureTypeBuilder#addIdentifier(Class)}</li>
-     *   <li>{@link FeatureTypeBuilder#addDefaultGeometry(Class)}</li>
-     *   <li>{@link FeatureTypeBuilder#addAttribute(Class)}</li>
-     *   <li>{@link FeatureTypeBuilder#addAssociation(FeatureType)}</li>
-     *   <li>{@link FeatureTypeBuilder#addAssociation(GenericName)}</li>
-     * </ul>
-     *
-     * @param <V> the class of property values.
-     */
-    public final class Property<V> extends Builder<Property<V>> {
-        /**
-         * The class of property values. Can not be changed after construction
-         * because this value determines the parameterized type {@code <V>}.
-         */
-        private final Class<V> valueClass;
-
-        /**
-         * The default value for the property, or {@code null} if none.
-         */
-        private V defaultValue;
-
-        /**
-         * The minimum number of property values.
-         * The default value is 1, unless otherwise specified by {@link #setDefaultCardinality(int, int)}.
-         *
-         * @see #setCardinality(int, int)
-         */
-        private int minimumOccurs;
-
-        /**
-         * The maximum number of property values. The default value is 1.
-         * The default value is 1, unless otherwise specified by {@link #setDefaultCardinality(int, int)}.
-         *
-         * @see #setCardinality(int, int)
-         */
-        private int maximumOccurs;
-
-        /**
-         * Builders for the characteristics associated to the attribute.
-         */
-        private final List<Characteristic<?>> characteristics;
-
-        /**
-         * Creates a new {@code AttributeType} or {@code Operation} builder for values of the given class.
-         *
-         * @param valueClass the class of property values.
-         */
-        Property(final Class<V> valueClass) {
-            this.valueClass = valueClass;
-            minimumOccurs   = defaultMinimumOccurs;
-            maximumOccurs   = defaultMaximumOccurs;
-            characteristics = new ArrayList<Characteristic<?>>();
-        }
-
-        /**
-         * Creates a new {@code AssociationRole} builder for values of the given type.
-         * This constructor arbitrarily stores the feature type as an unnamed characteristic of this property.
-         *
-         * @param valueClass shall be {@code Feature.class}.
-         * @param typeClass  shall be either {@code FeatureType.class} or {@code GenericName.class}.
-         * @param type       the type of associated features.
-         */
-        <C> Property(final Class<V> valueClass, final Class<C> typeClass, final C type) {
-            this.valueClass = valueClass;
-            minimumOccurs   = defaultMinimumOccurs;
-            maximumOccurs   = defaultMaximumOccurs;
-            characteristics = Collections.<Characteristic<?>>singletonList(new Characteristic<C>(typeClass).setDefaultValue(type));
-        }
-
-        /**
-         * Delegates the creation of a new name to the enclosing builder.
-         */
-        @Override
-        final GenericName name(final String scope, final String localPart) {
-            return FeatureTypeBuilder.this.name(scope, localPart);
-        }
-
-        /**
-         * Sets the minimum and maximum number of property values. Those numbers must be equal or greater than zero.
-         *
-         * <p>If this method is not invoked, then the default values are the cardinality specified by the last call
-         * to {@link #setDefaultCardinality(int, int)} at the time this {@code Property} instance has been created.
-         * If the later method has not invoked neither, then the default cardinality is [1 … 1].</p>
-         *
-         * @param  minimumOccurs  new minimum number of property values.
-         * @param  maximumOccurs  new maximum number of property values.
-         * @return {@code this} for allowing method calls chaining.
-         */
-        public Property<V> setCardinality(final int minimumOccurs, final int maximumOccurs) {
-            if (minimumOccurs < 0 || maximumOccurs < minimumOccurs) {
-                throw new IllegalArgumentException(errors().getString(Errors.Keys.IllegalRange_2, minimumOccurs, maximumOccurs));
-            }
-            this.minimumOccurs = minimumOccurs;
-            this.maximumOccurs = maximumOccurs;
-            return this;
-        }
-
-        /**
-         * Sets the default value for the property.
-         *
-         * @param  defaultValue  default property value, or {@code null} if none.
-         * @return {@code this} for allowing method calls chaining.
-         */
-        public Property<V> setDefaultValue(final V defaultValue) {
-            this.defaultValue = defaultValue;
-            return this;
-        }
-
-        /**
-         * Sets an enumeration of valid values for this attribute.
-         *
-         * <p>This is a convenience method for {@link #addCharacteristic(Class)} with a value
-         * of type {@link Set} and a conventional name.</p>
-         *
-         * @param  values valid values.
-         * @return {@code this} for allowing method calls chaining.
-         * @throws UnsupportedOperationException if this property does not support characteristics.
-         *
-         * @see AttributeConvention#VALID_VALUES_CHARACTERISTIC
-         */
-        public final Property<V> setValidValues(final V... values) {
-            return setCharacteristic(AttributeConvention.VALID_VALUES_CHARACTERISTIC,
-                    Set.class, CollectionsExt.immutableSet(false, values));
-        }
-
-        /**
-         * Sets the maximal length that characterizes the {@link CharSequence} values of this attribute.
-         * While this characteristic can be applied to any kind of attribute, it is meaningful only with
-         * character sequences.
-         *
-         * <p>This is a convenience method for {@link #addCharacteristic(Class)} with a value
-         * of type {@link Integer} and a conventional name.</p>
-         *
-         * @param  length  maximal length of {@link CharSequence} attribute values, or {@code null}.
-         * @return {@code this} for allowing method calls chaining.
-         * @throws UnsupportedOperationException if this property does not support length characteristics.
-         *
-         * @see AttributeConvention#MAXIMAL_LENGTH_CHARACTERISTIC
-         */
-        public Property<V> setMaximalLengthCharacteristic(final Integer length) {
-            return setCharacteristic(AttributeConvention.MAXIMAL_LENGTH_CHARACTERISTIC, Integer.class, length);
-        }
-
-        /**
-         * Sets the coordinate reference system that characterizes the values of this attribute.
-         * While this characteristic can be applied to any kind of attribute, it is meaningful
-         * only with georeferenced values like geometries or coverages.
-         *
-         * <p>This is a convenience method for {@link #addCharacteristic(Class)} with a value
-         * of type {@link CoordinateReferenceSystem} and a conventional name.</p>
-         *
-         * @param  crs  coordinate reference system associated to attribute values, or {@code null}.
-         * @return {@code this} for allowing method calls chaining.
-         * @throws UnsupportedOperationException if this property does not support CRS characteristics.
-         *
-         * @see AttributeConvention#CRS_CHARACTERISTIC
-         */
-        public Property<V> setCRSCharacteristic(final CoordinateReferenceSystem crs) {
-            return setCharacteristic(AttributeConvention.CRS_CHARACTERISTIC, CoordinateReferenceSystem.class, crs);
-        }
-
-        /**
-         * Implementation of all setter methods for characteristics.
-         *
-         * @throws UnsupportedOperationException if this property does not support characteristics.
-         */
-        private <C> Property<V> setCharacteristic(final GenericName name, final Class<C> type, final C value) {
-            for (final Characteristic<?> characteristic : characteristics) {
-                if (name.equals(characteristic.identification.get(DefaultAttributeType.NAME_KEY))) {
-                    characteristic.set(value);
-                    return this;
-                }
-            }
-            addCharacteristic(type).setDefaultValue(value).setName(name);
-            return this;
-        }
-
-        /**
-         * Adds another attribute type that describes this attribute type.
-         * See <cite>"Attribute characterization"</cite> in {@link DefaultAttributeType} Javadoc for more information.
-         *
-         * <p>Usage example:</p>
-         * {@preformat java
-         *     attribute.addCharacteristic(Unit.class).setName("Unit of measurement").setDefaultValue(SI.CELSIUS);
-         * }
-         *
-         * Callers shall invoke at least one of the {@code Characteristic.setName(…)} methods on the returned instance.
-         * All other methods are optional.
-         *
-         * @param  <C>   the compile-time type of {@code type} argument.
-         * @param  type  the class of characteristic values.
-         * @return a builder for a characteristic of this attribute.
-         * @throws UnsupportedOperationException if this property does not support characteristics.
-         */
-        public <C> Characteristic<C> addCharacteristic(final Class<C> type) {
-            if (valueClass == AbstractFeature.class) {
-                throw new UnsupportedOperationException(errors().getString(Errors.Keys.IllegalOperationForValueClass_1, valueClass));
-            }
-            ArgumentChecks.ensureNonNull("type", type);
-            final Characteristic<C> characteristic = new Characteristic<C>(type);
-            characteristics.add(characteristic);
-            return characteristic;
-        }
-
-        /**
-         * Creates a new property type from the current setting.
-         */
-        final AbstractIdentifiedType build() {
-            final AbstractIdentifiedType property;
-            if (valueClass == AbstractFeature.class) {
-                final Object type = CollectionsExt.first(characteristics).defaultValue;
-                if (type instanceof DefaultFeatureType) {
-                    property = new DefaultAssociationRole(identification, (DefaultFeatureType) type, minimumOccurs, maximumOccurs);
-                } else {
-                    property = new DefaultAssociationRole(identification, (GenericName) type, minimumOccurs, maximumOccurs);
-                }
-            } else {
-                final DefaultAttributeType<?>[] chrts = new DefaultAttributeType<?>[characteristics.size()];
-                for (int i=0; i<chrts.length; i++) {
-                    chrts[i] = characteristics.get(i).build();
-                }
-                property = new DefaultAttributeType<V>(identification, valueClass, minimumOccurs, maximumOccurs, defaultValue, chrts);
-            }
-            return property;
-        }
-    }
-
-    /**
-     * Describes one characteristic of the {@code AttributeType} to be built by the enclosing {@code FeatureTypeBuilder}.
-     * A different instance of {@code Characteristic} exists for each characteristic to describe.
-     * Those instances are created by:
-     *
-     * <ul>
-     *   <li>{@link Property#addCharacteristic(Class)}</li>
-     * </ul>
-     *
-     * @param <V> the class of characteristic values.
-     */
-    public final class Characteristic<V> extends Builder<Characteristic<V>> {
-        /**
-         * The class of attribute values. Can not be changed after construction
-         * because this value determines the parameterized type {@code <V>}.
-         */
-        private final Class<V> valueClass;
-
-        /**
-         * The default value for the attribute, or {@code null} if none.
-         */
-        V defaultValue;
-
-        /**
-         * Creates a new characteristic builder for values of the given class.
-         *
-         * @param valueClass the class of characteristic values.
-         */
-        Characteristic(final Class<V> valueClass) {
-            this.valueClass = valueClass;
-        }
-
-        /**
-         * Delegates the creation of a new name to the enclosing builder.
-         */
-        @Override
-        final GenericName name(final String scope, final String localPart) {
-            return FeatureTypeBuilder.this.name(scope, localPart);
-        }
-
-        /**
-         * Sets the default value with check of the value class.
-         */
-        final void set(final Object value) {
-            setDefaultValue(valueClass.cast(value));
-        }
-
-        /**
-         * Sets the default value for the characteristic.
-         *
-         * @param  value  characteristic default value, or {@code null} if none.
-         * @return {@code this} for allowing method calls chaining.
-         */
-        public Characteristic<V> setDefaultValue(final V value) {
-            defaultValue = value;
-            return this;
-        }
-
-        /**
-         * Creates a new characteristic from the current setting.
-         */
-        final DefaultAttributeType<V> build() {
-            return new DefaultAttributeType<V>(identification, valueClass, 0, 1, defaultValue);
-        }
-    }
-
-    /**
-     * Builds the feature type from the information and properties specified to this builder.
-     * One of the {@code setName(…)} methods must have been invoked before this {@code build()} method (mandatory).
-     * All other methods are optional, but some calls to a {@code add} method are usually needed.
-     *
-     * @return the new feature type.
-     * @throws IllegalStateException if the feature type contains incompatible
-     *         {@linkplain Property#setCRSCharacteristic CRS characteristics}.
-     */
-    public DefaultFeatureType build() throws IllegalStateException {
-        int numSynthetic;                                   // Number of synthetic properties to be generated.
-        int numSpecified = properties.size();               // Number of explicitely specified properties.
-        final AbstractIdentifiedType[] identifierTypes;
-        if (idAttributes.isEmpty()) {
-            identifierTypes = null;
-            numSynthetic = 0;
-        } else {
-            identifierTypes = new AbstractIdentifiedType[idAttributes.size()];
-            numSynthetic = 1;                               // Reserve one slot for the synthetic property "@id".
-        }
-        if (defaultGeometry != null) {
-            numSynthetic += 2;                              // One slot for "@defaultGeometry" and one for "@envelope".
-        }
-        AbstractIdentifiedType[] propertyTypes = new AbstractIdentifiedType[numSynthetic + numSpecified];
-        for (int i=0,j=numSynthetic; i<numSpecified; i++, j++) {
-            final Property<?>  builder  = properties.get(i);
-            final AbstractIdentifiedType instance = builder.build();
-            propertyTypes[j] = instance;
-            final int id = idAttributes.indexOf(builder);
-            if (id >= 0) {
-                identifierTypes[id] = instance;
-            }
-            /*
-             * If there is a default geometry, add a link named "@geometry" to that geometry. It may happen
-             * that the property created by the user is already named "@geometry", in which case will will
-             * avoid to duplicate the property by removing the second occurrence.
-             */
-            if (builder == defaultGeometry) {
-                final AbstractIdentifiedType geom;
-                if (AttributeConvention.DEFAULT_GEOMETRY_PROPERTY.equals(instance.getName())) {
-                    propertyTypes = ArraysExt.remove(propertyTypes, j--, 1);
-                    geom = instance;
-                } else {
-                    geom = FeatureOperations.link(name(AttributeConvention.DEFAULT_GEOMETRY_PROPERTY), instance);
-                }
-                propertyTypes[numSynthetic - 1] = geom;
-            }
-        }
-        /*
-         * Create the "envelope" operation only after we created all other properties, except "@id" which is not needed
-         * for envelope. It is okay if the 'propertyTypes' array still contains null elements like the "@id" one, since
-         * FeatureOperations.envelope(…) constructor will ignore any property which is not for a value.
-         */
-        if (defaultGeometry != null) try {
-            propertyTypes[numSynthetic - 2] = FeatureOperations.envelope(name(AttributeConvention.ENVELOPE_PROPERTY), null, propertyTypes);
-        } catch (FactoryException e) {
-            throw new IllegalStateException(e);
-        }
-        if (identifierTypes != null) {
-            propertyTypes[0] = FeatureOperations.compound(name(AttributeConvention.ID_PROPERTY), idDelimiter, idPrefix, idSuffix, identifierTypes);
-        }
-        return new DefaultFeatureType(identification, isAbstract, superTypes.toArray(new DefaultFeatureType[superTypes.size()]), propertyTypes);
-    }
-
-    /**
-     * Helper method for creating identification info of synthetic attributes.
-     */
-    private static Map<String,?> name(final GenericName name) {
-        return Collections.singletonMap(AbstractOperation.NAME_KEY, name);
-    }
-
-    /**
-     * Invoked by {@link Builder} for creating new {@code LocalName} or {@code GenericName} instances.
-     */
-    @Override
-    final GenericName name(String scope, final String localPart) {
-        if (scope == null) {
-            scope = defaultScope;
-        }
-        if (scope == null || scope.isEmpty()) {
-            return nameFactory.createLocalName(null, localPart);
-        } else {
-            return nameFactory.createGenericName(null, scope, localPart);
-        }
-    }
-
-    /**
-     * Returns the resources for error messages.
-     */
-    final Errors errors() {
-        return Errors.getResources(identification);
-    }
-}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Geometries.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Geometries.java
index 84cd269..e66a385 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Geometries.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Geometries.java
@@ -16,10 +16,15 @@
  */
 package org.apache.sis.internal.feature;
 
+import java.util.logging.Level;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
 import com.esri.core.geometry.Geometry;
 import com.esri.core.geometry.Envelope2D;
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.util.Static;
+import org.apache.sis.util.logging.Logging;
+import org.apache.sis.internal.system.Loggers;
 
 
 /**
@@ -32,11 +37,52 @@
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  * @since   0.7
- * @version 0.7
+ * @version 0.8
  * @module
  */
 public final class Geometries extends Static {
     /**
+     * The geometry object from Java Topology Suite (JTS),
+     * or {@code null} if the JTS library is not on the classpath.
+     */
+    private static final Class<?> JTS;
+
+    /**
+     * Getter methods on JTS envelopes, or {@code null} if the JTS library is not on the classpath.
+     * Each methods take no argument and return a {@code double} value.
+     */
+    private static final Method INTERNAL, MIN_X, MIN_Y, MAX_X, MAX_Y;
+
+    static {
+        Class<?> type;
+        Method genv, xmin, ymin, xmax, ymax;
+        try {
+            final Class<?> envt;
+            type = Class.forName("com.vividsolutions.jts.geom.Geometry");
+            genv = type.getMethod("getEnvelopeInternal", (Class[]) null);
+            envt = genv.getReturnType();
+            xmin = envt.getMethod("getMinX", (Class[]) null);
+            ymin = envt.getMethod("getMinY", (Class[]) null);
+            xmax = envt.getMethod("getMaxX", (Class[]) null);
+            ymax = envt.getMethod("getMaxY", (Class[]) null);
+        } catch (Exception e) {     // (ClassNotFoundException | NoSuchMethodException) on the JDK7 branch.
+            Logging.getLogger(Loggers.GEOMETRY).log(Level.CONFIG, e.toString());
+            type = null;
+            genv = null;
+            xmin = null;
+            xmax = null;
+            ymin = null;
+            ymax = null;
+        }
+        JTS = type;
+        INTERNAL = genv;
+        MIN_X = xmin;
+        MIN_Y = ymin;
+        MAX_X = xmax;
+        MAX_Y = ymax;
+    }
+
+    /**
      * Do not allow instantiation of this class.
      */
     private Geometries() {
@@ -49,7 +95,7 @@
      * @return {@code true} if the given type is one of the geometry type known to SIS.
      */
     public static boolean isKnownType(final Class<?> type) {
-        return Geometry.class.isAssignableFrom(type);
+        return Geometry.class.isAssignableFrom(type) || (JTS != null && JTS.isAssignableFrom(type));
     }
 
     /**
@@ -61,16 +107,43 @@
      *         a recognized geometry or its envelope is empty.
      */
     public static GeneralEnvelope getEnvelope(final Object geometry) {
+        final double xmin, ymin, xmax, ymax;
         if (geometry instanceof Geometry) {
             final Envelope2D bounds = new Envelope2D();
             ((Geometry) geometry).queryEnvelope2D(bounds);
-            if (!bounds.isEmpty()) {                                    // Test if there is NaN values.
-                final GeneralEnvelope env = new GeneralEnvelope(2);
-                env.setRange(0, bounds.xmin, bounds.xmax);
-                env.setRange(1, bounds.ymin, bounds.ymax);
-                return env;
+            if (bounds.isEmpty()) {                                     // Test if there is NaN values.
+                return null;
             }
+            xmin = bounds.xmin;
+            ymin = bounds.ymin;
+            xmax = bounds.xmax;
+            ymax = bounds.ymax;
+        } else if (JTS != null && JTS.isInstance(geometry)) {
+            try {
+                final Object env = INTERNAL.invoke(geometry, (Object[]) null);
+                xmin = (Double) MIN_X.invoke(env, (Object[]) null);
+                ymin = (Double) MIN_Y.invoke(env, (Object[]) null);
+                xmax = (Double) MAX_X.invoke(env, (Object[]) null);
+                ymax = (Double) MAX_Y.invoke(env, (Object[]) null);
+            } catch (ReflectiveOperationException e) {
+                if (e instanceof InvocationTargetException) {
+                    final Throwable cause = e.getCause();
+                    if (cause instanceof RuntimeException) {
+                        throw (RuntimeException) cause;
+                    }
+                    if (cause instanceof Error) {
+                        throw (Error) cause;
+                    }
+                }
+                // Should never happen unless JTS's API changed.
+                throw (Error) new IncompatibleClassChangeError(e.toString()).initCause(e);
+            }
+        } else {
+            return null;
         }
-        return null;
+        final GeneralEnvelope env = new GeneralEnvelope(2);
+        env.setRange(0, xmin, xmax);
+        env.setRange(1, ymin, ymax);
+        return env;
     }
 }
diff --git a/core/sis-feature/src/test/java/org/apache/sis/feature/AbstractOperationTest.java b/core/sis-feature/src/test/java/org/apache/sis/feature/AbstractOperationTest.java
index 85c60b6..e12bbb9 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/feature/AbstractOperationTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/feature/AbstractOperationTest.java
@@ -31,7 +31,7 @@
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @since   0.6
- * @version 0.6
+ * @version 0.8
  * @module
  */
 @DependsOn(SingletonAttributeTest.class)
@@ -62,6 +62,6 @@
      */
     @Test
     public void testToString() {
-        assertEquals("Operation[“found city” (founder) : city]", foundCity().toString());
+        assertEquals("NoOperation[“found city” (founder) : String]", foundCity().toString());
     }
 }
diff --git a/core/sis-feature/src/test/java/org/apache/sis/feature/EnvelopeOperationTest.java b/core/sis-feature/src/test/java/org/apache/sis/feature/EnvelopeOperationTest.java
index 00942b3..3c9b4ff 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/feature/EnvelopeOperationTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/feature/EnvelopeOperationTest.java
@@ -78,7 +78,7 @@
             null,
             null
         };
-        attributes[4] = FeatureOperations.link(name(AttributeConvention.DEFAULT_GEOMETRY_PROPERTY), attributes[defaultGeometry]);
+        attributes[4] = FeatureOperations.link(name(AttributeConvention.GEOMETRY_PROPERTY), attributes[defaultGeometry]);
         attributes[5] = FeatureOperations.envelope(name("bounds"), null, attributes);
         return new DefaultFeatureType(name("school"), false, null, attributes);
     }
diff --git a/core/sis-feature/src/test/java/org/apache/sis/internal/feature/FeatureTypeBuilderTest.java b/core/sis-feature/src/test/java/org/apache/sis/feature/builder/FeatureTypeBuilderTest.java
similarity index 80%
rename from core/sis-feature/src/test/java/org/apache/sis/internal/feature/FeatureTypeBuilderTest.java
rename to core/sis-feature/src/test/java/org/apache/sis/feature/builder/FeatureTypeBuilderTest.java
index 28052fa..6874c8b 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/internal/feature/FeatureTypeBuilderTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/feature/builder/FeatureTypeBuilderTest.java
@@ -14,13 +14,16 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.feature;
+package org.apache.sis.feature.builder;
 
 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;
 
@@ -37,8 +40,8 @@
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @since   0.7
- * @version 0.7
+ * @since   0.8
+ * @version 0.8
  * @module
  */
 public final strictfp class FeatureTypeBuilderTest extends TestCase {
@@ -47,25 +50,20 @@
      */
     @Test
     public void testEmptyProperty() {
-        final FeatureTypeBuilder.Property<String> builder = new FeatureTypeBuilder().addAttribute(String.class);
-        try {
-            builder.build();
-            fail("Builder should have failed if there is not at least a name set.");
-        } catch (IllegalArgumentException ex) {
-            final String message = ex.getMessage();
-            assertTrue(message, message.contains("name"));
-        }
+        final AttributeTypeBuilder<String> builder = new FeatureTypeBuilder().addAttribute(String.class);
+        assertEquals("default name", "string", builder.getName().toString());
+
         builder.setName("myScope", "myName");
         final DefaultAttributeType<?> att = (DefaultAttributeType<?>) builder.build();
 
-        assertEquals("name",       "myScope:myName", att.getName().toString());
-        assertEquals("valueClass", String.class,     att.getValueClass());
-        assertNull  ("defaultValue",                 att.getDefaultValue());
-        assertNull  ("definition",                   att.getDefinition());
-        assertNull  ("description",                  att.getDescription());
-        assertNull  ("designation",                  att.getDesignation());
-        assertEquals("minimumOccurs", 1,             att.getMinimumOccurs());
-        assertEquals("maximumOccurs", 1,             att.getMaximumOccurs());
+        assertEquals("name", "myScope:myName",   att.getName().toString());
+        assertEquals("valueClass", String.class, att.getValueClass());
+        assertNull  ("defaultValue",             att.getDefaultValue());
+        assertNull  ("definition",               att.getDefinition());
+        assertNull  ("description",              att.getDescription());
+        assertNull  ("designation",              att.getDesignation());
+        assertEquals("minimumOccurs", 1,         att.getMinimumOccurs());
+        assertEquals("maximumOccurs", 1,         att.getMaximumOccurs());
     }
 
     /**
@@ -104,14 +102,14 @@
     @Test
     @DependsOnMethod("testEmptyProperty")
     public void testPropertyBuild() {
-        final FeatureTypeBuilder.Property<String> builder = new FeatureTypeBuilder().addAttribute(String.class);
+        final AttributeTypeBuilder<String> builder = new FeatureTypeBuilder().addAttribute(String.class);
         builder.setName        ("myScope", "myName");
         builder.setDefinition  ("test definition");
         builder.setDesignation ("test designation");
         builder.setDescription ("test description");
         builder.setDefaultValue("test default value.");
         builder.setCardinality(10, 60);
-        builder.setMaximalLengthCharacteristic(80);
+        builder.setMaximalLength(80);
         final DefaultAttributeType<?> att = (DefaultAttributeType<?>) builder.build();
 
         assertEquals("name",          "myScope:myName",      att.getName().toString());
@@ -147,8 +145,8 @@
         builder.setAbstract(true);
         builder.addAttribute(String .class).setName("name");
         builder.addAttribute(Integer.class).setName("age");
-        builder.addAttribute(Point  .class).setName("location").setCRSCharacteristic(HardCodedCRS.WGS84);
-        builder.addAttribute(Double .class).setName("score").setCardinality(5, 50).setDefaultValue(10.0);
+        builder.addAttribute(Point  .class).setName("location").setCRS(HardCodedCRS.WGS84);
+        builder.addAttribute(Double .class).setName("score").setDefaultValue(10.0).setCardinality(5, 50);
 
         final DefaultFeatureType type = builder.build();
         assertEquals("name",        "myScope:myName",   type.getName().toString());
@@ -196,18 +194,6 @@
     }
 
     /**
-     * Test {@link FeatureTypeBuilder#clear()}.
-     */
-    @Test
-    @DependsOnMethod({"testEmptyFeature", "testAddProperties"})
-    public void testClear() {
-        final FeatureTypeBuilder builder = new FeatureTypeBuilder();
-        testAddProperties(builder);
-        builder.clear();
-        testEmptyFeature(builder);
-    }
-
-    /**
      * Tests {@link FeatureTypeBuilder#addIdentifier(Class)}.
      */
     @Test
@@ -216,8 +202,11 @@
         final FeatureTypeBuilder builder = new FeatureTypeBuilder();
         builder.setName("scope", "test");
         builder.setIdentifierDelimiters("-", "pref.", null);
-        builder.addIdentifier(String.class).setName("name");
-        builder.addDefaultGeometry(Geometry.class).setName("shape").setCRSCharacteristic(HardCodedCRS.WGS84);
+        builder.addAttribute(String.class).setName("name")
+                .addRole(AttributeRole.IDENTIFIER_COMPONENT);
+        builder.addAttribute(Geometry.class).setName("shape")
+                .setCRS(HardCodedCRS.WGS84)
+                .addRole(AttributeRole.DEFAULT_GEOMETRY);
 
         final DefaultFeatureType type = builder.build();
         assertEquals("name", "scope:test", type.getName().toString());
@@ -231,10 +220,22 @@
         final AbstractIdentifiedType a4 = it.next();
         assertFalse("properties count", it.hasNext());
 
-        assertEquals("name", AttributeConvention.ID_PROPERTY,                a0.getName());
-        assertEquals("name", AttributeConvention.ENVELOPE_PROPERTY,          a1.getName());
-        assertEquals("name", AttributeConvention.DEFAULT_GEOMETRY_PROPERTY,  a2.getName());
-        assertEquals("name", "name",                                         a3.getName().toString());
-        assertEquals("name", "shape",                                        a4.getName().toString());
+        assertEquals("name", AttributeConvention.IDENTIFIER_PROPERTY, a0.getName());
+        assertEquals("name", AttributeConvention.ENVELOPE_PROPERTY,   a1.getName());
+        assertEquals("name", AttributeConvention.GEOMETRY_PROPERTY,   a2.getName());
+        assertEquals("name", "name",                                  a3.getName().toString());
+        assertEquals("name", "shape",                                 a4.getName().toString());
+    }
+
+    /**
+     * Tests creation of a builder from an existing feature type.
+     * This method also acts as a test of {@code FeatureTypeBuilder} getter methods.
+     */
+    @Test
+    public void testCreateFromTemplate() {
+        final FeatureTypeBuilder builder = new FeatureTypeBuilder(DefaultFeatureTypeTest.capital());
+        assertEquals("name",       "Capital", builder.getName().toString());
+        assertEquals("superTypes", "City",    TestUtilities.getSingleton(builder.getSuperTypes()).getName().toString());
+        assertFalse ("isAbstract",            builder.isAbstract());
     }
 }
diff --git a/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java b/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
index 784e74d..9f4a3f1 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
@@ -49,7 +49,7 @@
     org.apache.sis.feature.FeatureFormatTest.class,
     org.apache.sis.feature.FeaturesTest.class,
     org.apache.sis.internal.feature.AttributeConventionTest.class,
-    org.apache.sis.internal.feature.FeatureTypeBuilderTest.class
+    org.apache.sis.feature.builder.FeatureTypeBuilderTest.class
 })
 public final strictfp class FeatureTestSuite extends TestSuite {
     /**
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/DefaultMetadata.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/DefaultMetadata.java
index 890a759..b732218 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/DefaultMetadata.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/DefaultMetadata.java
@@ -375,6 +375,11 @@
     /**
      * Returns a unique identifier for this metadata record.
      *
+     * <div class="note"><b>Note:</b>
+     * OGC 07-045 (Catalog Service Specification — ISO metadata application profile) recommends usage
+     * of a UUID (Universal Unique Identifier) as specified by <a href="http://www.ietf.org">IETF</a>
+     * to ensure identifier’s uniqueness.</div>
+     *
      * @return Unique identifier for this metadata record, or {@code null}.
      *
      * @since 0.5
@@ -611,7 +616,7 @@
 
     /**
      * Returns an identification of the parent metadata record.
-     * This is non-null if this metadata is a subset (child) of another metadata.
+     * This is non-null if this metadata is a subset (child) of another metadata that is described elsewhere.
      *
      * @return Identification of the parent metadata record, or {@code null} if none.
      *
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AbstractStereographic.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AbstractStereographic.java
index 5f7a86b..6eebeb1 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AbstractStereographic.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AbstractStereographic.java
@@ -31,7 +31,7 @@
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
  * @since   0.6
- * @version 0.6
+ * @version 0.8
  * @module
  */
 @XmlTransient
@@ -42,12 +42,6 @@
     private static final long serialVersionUID = -8797654778436582119L;
 
     /**
-     * The operation parameter descriptor for the <cite>Scale factor at natural origin</cite> (k₀) parameter value.
-     * Valid values range is (0 … ∞) and default value is 1.
-     */
-    public static final ParameterDescriptor<Double> SCALE_FACTOR = Mercator1SP.SCALE_FACTOR;
-
-    /**
      * The operation parameter descriptor for the <cite>False easting</cite> (FE) parameter value.
      * Valid values range is unrestricted and default value is 0 metre.
      */
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricAffine.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricAffine.java
index ab78a5f..9be7867 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricAffine.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricAffine.java
@@ -128,13 +128,13 @@
     static final ParameterDescriptor<Double> DS;
     static {
         final ParameterBuilder builder = builder();
-        TX = createShift(builder.addName("X-axis translation").addName(Citations.OGC, "dx"));
-        TY = createShift(builder.addName("Y-axis translation").addName(Citations.OGC, "dy"));
-        TZ = createShift(builder.addName("Z-axis translation").addName(Citations.OGC, "dz"));
-        RX = createRotation(builder, "X-axis rotation", "ex");
-        RY = createRotation(builder, "Y-axis rotation", "ey");
-        RZ = createRotation(builder, "Z-axis rotation", "ez");
-        DS = builder.addName("Scale difference").addName(Citations.OGC, "ppm").create(1, Units.PPM);
+        TX = createShift(builder.addIdentifier("8605").addName("X-axis translation").addName(Citations.OGC, "dx"));
+        TY = createShift(builder.addIdentifier("8606").addName("Y-axis translation").addName(Citations.OGC, "dy"));
+        TZ = createShift(builder.addIdentifier("8607").addName("Z-axis translation").addName(Citations.OGC, "dz"));
+        RX = createRotation(builder.addIdentifier("8608"), "X-axis rotation", "ex");
+        RY = createRotation(builder.addIdentifier("8609"), "Y-axis rotation", "ey");
+        RZ = createRotation(builder.addIdentifier("8610"), "Z-axis rotation", "ez");
+        DS = builder.addIdentifier("8611").addName("Scale difference").addName(Citations.OGC, "ppm").create(1, Units.PPM);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/LambertConformal2SP.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/LambertConformal2SP.java
index 2c16bc8..5d3a5b8 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/LambertConformal2SP.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/LambertConformal2SP.java
@@ -107,9 +107,8 @@
          * NetCDF:  longitude_of_central_meridian
          * GeoTIFF: FalseOriginLong
          */
-        LONGITUDE_OF_FALSE_ORIGIN = createLongitude(exceptEPSG(LambertConformal1SP.LONGITUDE_OF_ORIGIN, builder
-                .addIdentifier("8822")
-                .addName("Longitude of false origin"))
+        LONGITUDE_OF_FALSE_ORIGIN = createLongitude(
+                 rename(LambertConformal1SP.LONGITUDE_OF_ORIGIN, "8822", "Longitude of false origin", builder)
                 .rename(Citations.NETCDF, "longitude_of_central_meridian")
                 .rename(Citations.GEOTIFF, "FalseOriginLong"));
         /*
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/LambertCylindricalEqualArea.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/LambertCylindricalEqualArea.java
new file mode 100644
index 0000000..1d70b43
--- /dev/null
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/LambertCylindricalEqualArea.java
@@ -0,0 +1,117 @@
+/*
+ * 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.CylindricalProjection;
+import org.apache.sis.metadata.iso.citation.Citations;
+import org.apache.sis.parameter.Parameters;
+import org.apache.sis.referencing.operation.projection.NormalizedProjection;
+import org.apache.sis.referencing.operation.projection.CylindricalEqualArea;
+
+
+/**
+ * The provider for <cite>"Lambert Cylindrical Equal Area"</cite> projection (EPSG:9835).
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @since   0.8
+ * @version 0.8
+ * @module
+ *
+ * @see <a href="http://www.remotesensing.org/geotiff/proj_list/cylindrical_equal_area.html">Cylindrical Equal Area on RemoteSensing.org</a>
+ */
+@XmlTransient
+public final class LambertCylindricalEqualArea extends MapProjection {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = -672278344635217838L;
+
+    /**
+     * 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 0°.
+     */
+    public static final ParameterDescriptor<Double> STANDARD_PARALLEL = Equirectangular.STANDARD_PARALLEL;
+
+    /**
+     * The operation parameter descriptor for the <cite>Longitude of natural origin</cite> (λ₀) parameter value.
+     * Valid values range is [-180 … 180]° and default value is 0°.
+     */
+    public static final ParameterDescriptor<Double> LONGITUDE_OF_ORIGIN = Mercator1SP.LONGITUDE_OF_ORIGIN;
+
+    /**
+     * The operation parameter descriptor for the <cite>False easting</cite> (FE) parameter value.
+     * Valid values range is unrestricted and default value is 0 metre.
+     */
+    public static final ParameterDescriptor<Double> FALSE_EASTING = Equirectangular.FALSE_EASTING;
+
+    /**
+     * The operation parameter descriptor for the <cite>False northing</cite> (FN) parameter value.
+     * Valid values range is unrestricted and default value is 0 metre.
+     */
+    public static final ParameterDescriptor<Double> FALSE_NORTHING = Equirectangular.FALSE_NORTHING;
+
+    /**
+     * The group of all parameters expected by this coordinate operation.
+     */
+    static final ParameterDescriptorGroup PARAMETERS;
+    static {
+        PARAMETERS = builder()
+                .addIdentifier(             "9835")
+                .addName(                   "Lambert Cylindrical Equal Area")
+                .addName(Citations.OGC,     "Cylindrical_Equal_Area")
+                .addName(Citations.ESRI,    "Cylindrical_Equal_Area")
+                .addName(Citations.GEOTIFF, "CT_CylindricalEqualArea")
+                .addName(Citations.PROJ4,   "cea")
+                .addIdentifier(Citations.GEOTIFF, "28")
+                .createGroupForMapProjection(
+                        STANDARD_PARALLEL,
+                        LONGITUDE_OF_ORIGIN,
+                        Mercator2SP.SCALE_FACTOR,           // Not formally a Cylindrical Equal Area parameter.
+                        FALSE_EASTING,
+                        FALSE_NORTHING);
+    }
+
+    /**
+     * Constructs a new provider.
+     */
+    public LambertCylindricalEqualArea() {
+        super(PARAMETERS);
+    }
+
+    /**
+     * Returns the operation type for this map projection.
+     *
+     * @return {@code CylindricalProjection.class}
+     */
+    @Override
+    public final Class<CylindricalProjection> getOperationType() {
+        return CylindricalProjection.class;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return The map projection created from the given parameter values.
+     */
+    @Override
+    protected NormalizedProjection createProjection(final Parameters parameters) {
+        return new CylindricalEqualArea(this, parameters);
+    }
+}
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/LambertCylindricalEqualAreaSpherical.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/LambertCylindricalEqualAreaSpherical.java
new file mode 100644
index 0000000..4151836
--- /dev/null
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/LambertCylindricalEqualAreaSpherical.java
@@ -0,0 +1,89 @@
+/*
+ * 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.ParameterDescriptorGroup;
+import org.opengis.referencing.operation.CylindricalProjection;
+import org.apache.sis.parameter.Parameters;
+import org.apache.sis.referencing.operation.projection.NormalizedProjection;
+import org.apache.sis.referencing.operation.projection.CylindricalEqualArea;
+
+
+/**
+ * The provider for <cite>"Lambert Cylindrical Equal Area (Spherical)"</cite> projection (EPSG:9834).
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @since   0.8
+ * @version 0.8
+ * @module
+ */
+@XmlTransient
+public final class LambertCylindricalEqualAreaSpherical extends MapProjection {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = 1456941129750586197L;
+
+    /**
+     * The EPSG identifier, to be preferred to the name when available.
+     */
+    public static final String IDENTIFIER = "9834";
+
+    /**
+     * The group of all parameters expected by this coordinate operation.
+     */
+    static final ParameterDescriptorGroup PARAMETERS;
+    static {
+        PARAMETERS = builder()
+                .addIdentifier(IDENTIFIER)
+                .addName("Lambert Cylindrical Equal Area (Spherical)")
+                .createGroupForMapProjection(
+                        LambertCylindricalEqualArea.STANDARD_PARALLEL,
+                        LambertCylindricalEqualArea.LONGITUDE_OF_ORIGIN,
+                        Mercator2SP.SCALE_FACTOR,           // Not formally a Cylindrical Equal Area parameter.
+                        LambertCylindricalEqualArea.FALSE_EASTING,
+                        LambertCylindricalEqualArea.FALSE_NORTHING);
+    }
+
+    /**
+     * Constructs a new provider.
+     */
+    public LambertCylindricalEqualAreaSpherical() {
+        super(PARAMETERS);
+    }
+
+    /**
+     * Returns the operation type for this map projection.
+     *
+     * @return {@code CylindricalProjection.class}
+     */
+    @Override
+    public final Class<CylindricalProjection> getOperationType() {
+        return CylindricalProjection.class;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return The map projection created from the given parameter values.
+     */
+    @Override
+    protected NormalizedProjection createProjection(final Parameters parameters) {
+        return new CylindricalEqualArea(this, parameters);
+    }
+}
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/MapProjection.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/MapProjection.java
index 44d9432..d076a44 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/MapProjection.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/MapProjection.java
@@ -58,7 +58,7 @@
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @since   0.6
- * @version 0.7
+ * @version 0.8
  * @module
  */
 @XmlTransient
@@ -245,47 +245,105 @@
     }
 
     /**
-     * Copies all aliases and identifiers except the ones for the given authority.
-     * If the given replacement is non-null, then it will be used instead of the
-     * first occurrence of the omitted name.
+     * Rename the primary name and identifier of the given descriptor. Aliases are copied as-is.
      *
-     * <p>This method does not copy the primary name. It is caller's responsibility to add it first.</p>
+     * @param  template    the parameter from which to copy the aliases.
+     * @param  identifier  the new EPSG identifier.
+     * @param  name        the new EPSG name.
+     * @param  builder     an initially clean builder where to add the names.
+     * @return the given {@code builder}, for method call chaining.
      *
-     * @param  source      The parameter from which to copy the names.
-     * @param  except      The authority of the name to omit. Can not be EPSG.
-     * @param  replacement The name to use instead of the omitted one, or {@code null} if none.
-     * @param  builder     Where to add the names.
-     * @return The given {@code builder}, for method call chaining.
-     *
-     * @since 0.7
+     * @since 0.8
      */
-    static ParameterBuilder except(final ParameterDescriptor<Double> source, final Citation except,
-            GenericName replacement, final ParameterBuilder builder)
+    static ParameterBuilder rename(final ParameterDescriptor<?> template, final String identifier, final String name,
+            final ParameterBuilder builder)
     {
-        for (GenericName alias : source.getAlias()) {
-            if (((Identifier) alias).getAuthority() == except) {
-                if (replacement == null) continue;
-                alias = replacement;
-                replacement = null;
-            }
-            builder.addName(alias);
-        }
-        for (final ReferenceIdentifier id : source.getIdentifiers()) {
+        return exceptEPSG(template, builder.addIdentifier(identifier).addName(name));
+    }
+
+    /**
+     * Copies name, aliases and identifiers of the given {@code template}, except the alias of the given authority
+     * which is replaced by the alias of the same authority in {@code replacement}.
+     *
+     * @param  template     the parameter from which to copy names and identifiers.
+     * @param  toRename     authority of the alias to rename.
+     * @param  replacement  the parameter from which to get the new name for the alias to rename.
+     * @param  builder      an initially clean builder where to add the names and identifiers.
+     * @return the given {@code builder}, for method call chaining.
+     *
+     * @since 0.8
+     */
+    static ParameterBuilder renameAlias(final ParameterDescriptor<Double> template, final Citation toRename,
+            final ParameterDescriptor<Double> replacement, final ParameterBuilder builder)
+    {
+        copyAliases(template, toRename, sameNameAs(toRename, replacement), builder.addName(template.getName()));
+        for (final ReferenceIdentifier id : template.getIdentifiers()) {
             builder.addIdentifier(id);
         }
         return builder;
     }
 
     /**
-     * Copies all names except the EPSG one from the given parameter into the builder.
-     * The EPSG name is presumed the first name and identifier (this is not verified).
+     * Copies all aliases except the ones for the given authority. If the given replacement is non-null,
+     * then it will be used instead of the first occurrence of the omitted name.
      *
-     * @param  source  The parameter from which to copy the names.
-     * @param  builder Where to add the names.
-     * @return The given {@code builder}, for method call chaining.
+     * <p>This method does not copy the primary name. It is caller's responsibility to add it first.</p>
+     *
+     * @param  template     the parameter from which to copy the aliases.
+     * @param  exclude      the authority of the alias to omit. Can not be EPSG.
+     * @param  replacement  the alias to use instead of the omitted one, or {@code null} if none.
+     * @param  builder      where to add the aliases.
+     * @return the given {@code builder}, for method call chaining.
      */
-    static ParameterBuilder exceptEPSG(final ParameterDescriptor<?> source, final ParameterBuilder builder) {
-        for (final GenericName alias : source.getAlias()) {
+    private static ParameterBuilder copyAliases(final ParameterDescriptor<Double> template, final Citation exclude,
+            GenericName replacement, final ParameterBuilder builder)
+    {
+        for (GenericName alias : template.getAlias()) {
+            if (((Identifier) alias).getAuthority() == exclude) {
+                if (replacement == null) continue;
+                alias = replacement;
+                replacement = null;
+            }
+            builder.addName(alias);
+        }
+        return builder;
+    }
+
+    /**
+     * Copies all aliases and identifiers, but using the alias specified by the given authority as the primary name.
+     * The old primary name (usually the EPSG name) is discarded. Identifier are <strong>not</strong> copied, which
+     * usually implies that only the EPSG identifier is ignored (because it is usually the only parameter identifier).
+     *
+     * <p>This is a convenience method for defining the parameters of an ESRI-specific (or any other authority)
+     * projection using the EPSG parameters as template. Note that in the particular case where the desired
+     * authority is OGC, {@link #exceptEPSG(ParameterDescriptor, ParameterBuilder)} is more efficient.</p>
+     *
+     * @param  template    the parameter from which to copy the names.
+     * @param  authority   the authority to use for the primary name.
+     * @param  builder     an initially clean builder where to add the names.
+     * @return the given {@code builder}, for method call chaining.
+     *
+     * @since 0.8
+     */
+    static ParameterBuilder alternativeAuthority(final ParameterDescriptor<Double> template,
+            final Citation authority, final ParameterBuilder builder)
+    {
+        return copyAliases(template, authority, null, builder.addName(sameNameAs(authority, template)));
+    }
+
+    /**
+     * Copies all names except the EPSG one from the given parameter into the builder.
+     * The EPSG information are presumed to be the primary name and the only identifier (this is not verified).
+     *
+     * <p>If this method is invoking with a "clean" builder, then the result is to promote the first alias as
+     * the primary name. The first alias is usually the OGC name.</p>
+     *
+     * @param  template  the parameter from which to copy the names.
+     * @param  builder   where to add the names.
+     * @return the given {@code builder}, for method call chaining.
+     */
+    static ParameterBuilder exceptEPSG(final ParameterDescriptor<?> template, final ParameterBuilder builder) {
+        for (final GenericName alias : template.getAlias()) {
             builder.addName(alias);
         }
         return builder;
@@ -294,8 +352,8 @@
     /**
      * Creates a remarks for parameters that are not formally EPSG parameter.
      *
-     * @param  origin The name of the projection for where the parameter is formally used.
-     * @return A remarks saying that the parameter is actually defined in {@code origin}.
+     * @param  origin  the name of the projection for where the parameter is formally used.
+     * @return a remarks saying that the parameter is actually defined in {@code origin}.
      */
     static InternationalString notFormalParameter(final String origin) {
         return Messages.formatInternational(Messages.Keys.NotFormalProjectionParameter_1, origin);
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Mercator2SP.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Mercator2SP.java
index d921cdc..2ea5148 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Mercator2SP.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Mercator2SP.java
@@ -105,9 +105,9 @@
                 .addIdentifier(Citations.S57,       "8")
                 .createGroupForMapProjection(
                         STANDARD_PARALLEL,
-                        latitudeOfOrigin,       // Not formally a Mercator2SP parameter.
+                        latitudeOfOrigin,                   // Not formally a Mercator2SP parameter.
                         Mercator1SP.LONGITUDE_OF_ORIGIN,
-                        SCALE_FACTOR,           // Not formally a Mercator2SP parameter.
+                        SCALE_FACTOR,                       // Not formally a Mercator2SP parameter.
                         FALSE_EASTING,
                         FALSE_NORTHING);
     }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Molodensky.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Molodensky.java
index b0c3e6d..eeb1b69 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Molodensky.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Molodensky.java
@@ -92,8 +92,8 @@
     public static final ParameterDescriptorGroup PARAMETERS;
     static {
         final ParameterBuilder builder = builder();
-        AXIS_LENGTH_DIFFERENCE = builder.addName("Semi-major axis length difference").create(Double.NaN, SI.METRE);
-        FLATTENING_DIFFERENCE  = builder.addName("Flattening difference").createBounded(-1, +1, Double.NaN, Unit.ONE);
+        AXIS_LENGTH_DIFFERENCE = builder.addIdentifier("8654").addName("Semi-major axis length difference").create(Double.NaN, SI.METRE);
+        FLATTENING_DIFFERENCE  = builder.addIdentifier("8655").addName("Flattening difference").createBounded(-1, +1, Double.NaN, Unit.ONE);
         PARAMETERS = builder.setRequired(true)
                 .addIdentifier("9604")
                 .addName("Molodensky")
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/ObliqueStereographic.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/ObliqueStereographic.java
index 600ec4c..0d97a3b 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/ObliqueStereographic.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/ObliqueStereographic.java
@@ -31,7 +31,7 @@
  * @author  Rueben Schulz (UBC)
  * @author  Martin Desruisseaux (Geomatys)
  * @since   0.6
- * @version 0.7
+ * @version 0.8
  * @module
  *
  * @see <a href="http://www.remotesensing.org/geotiff/proj_list/oblique_stereographic.html">Oblique Stereographic on RemoteSensing.org</a>
@@ -56,6 +56,12 @@
     public static final ParameterDescriptor<Double> LONGITUDE_OF_ORIGIN = Mercator1SP.LONGITUDE_OF_ORIGIN;
 
     /**
+     * The operation parameter descriptor for the <cite>Scale factor at natural origin</cite> (k₀) parameter value.
+     * Valid values range is (0 … ∞) and default value is 1.
+     */
+    public static final ParameterDescriptor<Double> SCALE_FACTOR = Mercator1SP.SCALE_FACTOR;    // Same as PolarStereographicA.
+
+    /**
      * The group of all parameters expected by this coordinate operation.
      */
     private static final ParameterDescriptorGroup PARAMETERS;
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/PolarStereographicA.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/PolarStereographicA.java
index c3f12d7..e278d30 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/PolarStereographicA.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/PolarStereographicA.java
@@ -29,7 +29,7 @@
  * @author  Rueben Schulz (UBC)
  * @author  Martin Desruisseaux (Geomatys)
  * @since   0.6
- * @version 0.7
+ * @version 0.8
  * @module
  *
  * @see <a href="http://www.remotesensing.org/geotiff/proj_list/polar_stereographic.html">Polar Stereographic on RemoteSensing.org</a>
@@ -64,6 +64,12 @@
     public static final ParameterDescriptor<Double> LONGITUDE_OF_ORIGIN;
 
     /**
+     * The operation parameter descriptor for the <cite>Scale factor at natural origin</cite> (k₀) parameter value.
+     * Valid values range is (0 … ∞) and default value is 1.
+     */
+    public static final ParameterDescriptor<Double> SCALE_FACTOR = Mercator1SP.SCALE_FACTOR;
+
+    /**
      * The group of all parameters expected by this coordinate operation.
      */
     private static final ParameterDescriptorGroup PARAMETERS;
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/PolarStereographicB.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/PolarStereographicB.java
index f9e225b..99e8ea3 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/PolarStereographicB.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/PolarStereographicB.java
@@ -31,7 +31,7 @@
  * @author  Rueben Schulz (UBC)
  * @author  Martin Desruisseaux (Geomatys)
  * @since   0.6
- * @version 0.6
+ * @version 0.8
  * @module
  */
 @XmlTransient
@@ -67,7 +67,6 @@
      * because it is sometime used in Well Known Text (WKT). However it shall be interpreted as a
      * <cite>Scale factor at the standard parallel</cite> rather than at the natural origin.</p>
      */
-    @SuppressWarnings("FieldNameHidesFieldInSuperclass")
     static final ParameterDescriptor<Double> SCALE_FACTOR;
 
     /**
@@ -77,8 +76,7 @@
     static {
         final ParameterBuilder builder = builder();
         LONGITUDE_OF_ORIGIN = createLongitude(
-                exceptEPSG(PolarStereographicA.LONGITUDE_OF_ORIGIN,
-                builder.addIdentifier("8833").addName("Longitude of origin")));
+                rename(PolarStereographicA.LONGITUDE_OF_ORIGIN, "8833", "Longitude of origin", builder));
 
         STANDARD_PARALLEL = createMandatoryLatitude(builder
                 .addIdentifier("8832").addName("Latitude of standard parallel")
@@ -98,7 +96,7 @@
                 .createGroupForMapProjection(
                         STANDARD_PARALLEL,
                         LONGITUDE_OF_ORIGIN,
-                        SCALE_FACTOR,       // Not formally a parameter of this projection.
+                        SCALE_FACTOR,                   // Not formally a parameter of this projection.
                         FALSE_EASTING,
                         FALSE_NORTHING);
     }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/PolarStereographicSouth.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/PolarStereographicSouth.java
index 12c8284..6359602 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/PolarStereographicSouth.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/PolarStereographicSouth.java
@@ -44,22 +44,18 @@
     private static final long serialVersionUID = -6173635411676914083L;
 
     /**
-     * Copies all names and identifiers, but using the ESRI authority as the primary name.
-     * This is a convenience method for defining the parameters of an ESRI-specific projection
-     * using the EPSG parameters as template.
-     */
-    private static ParameterBuilder addNamesAndIdentifiers(final ParameterDescriptor<Double> source, final ParameterBuilder builder) {
-        return except(source, Citations.ESRI, null, builder.addName(sameNameAs(Citations.ESRI, source)).addName(source.getName()));
-    }
-
-    /**
-     * Returns the same parameter than the given one, except that the primary name is the ESRI name
-     * instead than the EPSG one.
+     * Returns the same parameter than the given one, except that the alias of the ESRI authority
+     * is promoted as the primary name. The old primary name and identifiers (which are usually the
+     * EPSG ones) are discarded.
+     *
+     * @param  template    the parameter from which to copy the names and identifiers.
+     * @param  builder     an initially clean builder where to add the names.
+     * @return the given {@code builder}, for method call chaining.
      */
     @SuppressWarnings("unchecked")
-    private static ParameterDescriptor<Double> forESRI(final ParameterDescriptor<Double> source, final ParameterBuilder builder) {
-        return addNamesAndIdentifiers(source, builder).createBounded((MeasurementRange<Double>)
-                ((DefaultParameterDescriptor<Double>) source).getValueDomain(), source.getDefaultValue());
+    private static ParameterDescriptor<Double> forESRI(final ParameterDescriptor<Double> template, final ParameterBuilder builder) {
+        return alternativeAuthority(template, Citations.ESRI, builder).createBounded((MeasurementRange<Double>)
+                ((DefaultParameterDescriptor<Double>) template).getValueDomain(), template.getDefaultValue());
     }
 
     /**
@@ -69,13 +65,13 @@
     static {
         final ParameterBuilder builder = builder();
         final ParameterDescriptor<?>[] parameters = {
-            addNamesAndIdentifiers(PolarStereographicB.STANDARD_PARALLEL, builder)
+            alternativeAuthority(PolarStereographicB.STANDARD_PARALLEL, Citations.ESRI, builder)
                    .createBounded(Latitude.MIN_VALUE, 0, Latitude.MIN_VALUE, NonSI.DEGREE_ANGLE),
 
             forESRI(PolarStereographicB.LONGITUDE_OF_ORIGIN, builder),
-            forESRI(PolarStereographicB.SCALE_FACTOR, builder),
-            forESRI(PolarStereographicB.FALSE_EASTING, builder),
-            forESRI(PolarStereographicB.FALSE_NORTHING, builder)
+                    PolarStereographicB.SCALE_FACTOR,                   // Not formally a parameter of this projection.
+            forESRI(LambertCylindricalEqualArea.FALSE_EASTING, builder),
+            forESRI(LambertCylindricalEqualArea.FALSE_NORTHING, builder)
         };
 
         PARAMETERS = builder
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/RegionalMercator.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/RegionalMercator.java
index 6894502..ee9271b 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/RegionalMercator.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/RegionalMercator.java
@@ -74,19 +74,16 @@
     static {
         final ParameterBuilder builder = builder();
 
-        LATITUDE_OF_FALSE_ORIGIN = createLatitude(exceptEPSG(Mercator1SP.LATITUDE_OF_ORIGIN, builder
-                .addIdentifier("8821")
-                .addName("Latitude of false origin"))
+        LATITUDE_OF_FALSE_ORIGIN = createLatitude(
+                 rename(Mercator1SP.LATITUDE_OF_ORIGIN, "8821", "Latitude of false origin", builder)
                 .rename(Citations.GEOTIFF, "FalseOriginLat"), false);
 
-        EASTING_AT_FALSE_ORIGIN = createShift(exceptEPSG(FALSE_EASTING, builder
-                .addIdentifier("8826")
-                .addName("Easting at false origin"))
+        EASTING_AT_FALSE_ORIGIN = createShift(
+                 rename(FALSE_EASTING, "8826", "Easting at false origin", builder)
                 .rename(Citations.GEOTIFF, "FalseOriginEasting"));
 
-        NORTHING_AT_FALSE_ORIGIN = createShift(exceptEPSG(FALSE_NORTHING, builder
-                .addIdentifier("8827")
-                .addName("Northing at false origin"))
+        NORTHING_AT_FALSE_ORIGIN = createShift(
+                 rename(FALSE_NORTHING, "8827", "Northing at false origin", builder)
                 .rename(Citations.GEOTIFF, "FalseOriginNorthing"));
 
         PARAMETERS = builder
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/TransverseMercator.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/TransverseMercator.java
index 5c6df3d..1849ce9 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/TransverseMercator.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/TransverseMercator.java
@@ -90,9 +90,8 @@
         LATITUDE_OF_ORIGIN = createLatitude(builder
                 .addNamesAndIdentifiers(Mercator1SP.LATITUDE_OF_ORIGIN), true);
 
-        builder.addName(Mercator1SP.LONGITUDE_OF_ORIGIN.getName());
-        LONGITUDE_OF_ORIGIN = createLongitude(except(Mercator1SP.LONGITUDE_OF_ORIGIN, Citations.NETCDF,
-                sameNameAs(Citations.NETCDF, LambertConformal2SP.LONGITUDE_OF_FALSE_ORIGIN), builder));
+        LONGITUDE_OF_ORIGIN = createLongitude(renameAlias(Mercator1SP.LONGITUDE_OF_ORIGIN,
+                Citations.NETCDF, LambertConformal2SP.LONGITUDE_OF_FALSE_ORIGIN, builder));
 
         SCALE_FACTOR = createScale(builder
                 .addNamesAndIdentifiers(Mercator1SP.SCALE_FACTOR)
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/package-info.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/package-info.java
index 77ea846..d71ee76 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/package-info.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/package-info.java
@@ -22,7 +22,7 @@
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @since   0.6
- * @version 0.7
+ * @version 0.8
  * @module
  *
  * @see org.apache.sis.referencing.operation.transform.MathTransformProvider
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/parameter/ParameterBuilder.java b/core/sis-referencing/src/main/java/org/apache/sis/parameter/ParameterBuilder.java
index 514b046..0481f40 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/parameter/ParameterBuilder.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/parameter/ParameterBuilder.java
@@ -125,7 +125,7 @@
     /**
      * Creates a new builder initialized to properties of the given object.
      *
-     * @param descriptor The descriptor from which to inherit properties, or {@code null}.
+     * @param descriptor  the descriptor from which to inherit properties, or {@code null}.
      *
      * @since 0.6
      */
@@ -165,10 +165,10 @@
     /**
      * Creates a descriptor for values of the given type without domain restriction.
      *
-     * @param  <T>          The compile-time type of the {@code valueClass} argument.
-     * @param  valueClass   The class that describe the type of the parameter values.
-     * @param  defaultValue The default value for the parameter, or {@code null} if none.
-     * @return The parameter descriptor for the given default value and unit.
+     * @param  <T>           the compile-time type of the {@code valueClass} argument.
+     * @param  valueClass    the class that describe the type of the parameter values.
+     * @param  defaultValue  the default value for the parameter, or {@code null} if none.
+     * @return the parameter descriptor for the given default value and unit.
      */
     public <T> ParameterDescriptor<T> create(final Class<T> valueClass, final T defaultValue) {
         return create(valueClass, null, null, defaultValue);
@@ -178,9 +178,9 @@
      * Creates a descriptor for floating point values without domain restriction.
      * All {@code double} values are considered valid.
      *
-     * @param  defaultValue The default value for the parameter, or {@link Double#NaN} if none.
-     * @param  unit         The default unit, or {@code null} if none.
-     * @return The parameter descriptor for the given default value and unit.
+     * @param  defaultValue  the default value for the parameter, or {@link Double#NaN} if none.
+     * @param  unit          the default unit, or {@code null} if none.
+     * @return the parameter descriptor for the given default value and unit.
      */
     public ParameterDescriptor<Double> create(final double defaultValue, final Unit<?> unit) {
         final Range<Double> valueDomain;
@@ -196,9 +196,9 @@
      * Creates a descriptor for floating point values greater than zero.
      * The zero value is not considered valid. There is no maximal value.
      *
-     * @param  defaultValue The default value for the parameter, or {@link Double#NaN} if none.
-     * @param  unit         The default unit, or {@code null} if none.
-     * @return The parameter descriptor for the given default value and unit.
+     * @param  defaultValue  the default value for the parameter, or {@link Double#NaN} if none.
+     * @param  unit          the default unit, or {@code null} if none.
+     * @return the parameter descriptor for the given default value and unit.
      */
     public ParameterDescriptor<Double> createStrictlyPositive(final double defaultValue, final Unit<?> unit) {
         final Range<Double> valueDomain;
@@ -213,11 +213,11 @@
     /**
      * Creates a descriptor for floating point values restricted to the given domain.
      *
-     * @param  minimumValue The minimum parameter value (inclusive), or {@link Double#NEGATIVE_INFINITY} if none.
-     * @param  maximumValue The maximum parameter value (inclusive), or {@link Double#POSITIVE_INFINITY} if none.
-     * @param  defaultValue The default value for the parameter, or {@link Double#NaN} if none.
-     * @param  unit         The unit for default, minimum and maximum values, or {@code null} if none.
-     * @return The parameter descriptor for the given domain of values.
+     * @param  minimumValue  the minimum parameter value (inclusive), or {@link Double#NEGATIVE_INFINITY} if none.
+     * @param  maximumValue  the maximum parameter value (inclusive), or {@link Double#POSITIVE_INFINITY} if none.
+     * @param  defaultValue  the default value for the parameter, or {@link Double#NaN} if none.
+     * @param  unit          the unit for default, minimum and maximum values, or {@code null} if none.
+     * @return the parameter descriptor for the given domain of values.
      */
     public ParameterDescriptor<Double> createBounded(final double minimumValue, final double maximumValue,
             final double defaultValue, final Unit<?> unit)
@@ -236,10 +236,10 @@
     /**
      * Creates a descriptor for integer values restricted to the given domain.
      *
-     * @param  minimumValue The minimum parameter value (inclusive).
-     * @param  maximumValue The maximum parameter value (inclusive).
-     * @param  defaultValue The default value for the parameter.
-     * @return The parameter descriptor for the given domain of values.
+     * @param  minimumValue  the minimum parameter value (inclusive).
+     * @param  maximumValue  the maximum parameter value (inclusive).
+     * @param  defaultValue  the default value for the parameter.
+     * @return the parameter descriptor for the given domain of values.
      */
     public ParameterDescriptor<Integer> createBounded(final int minimumValue, final int maximumValue,
             final int defaultValue)
@@ -250,12 +250,12 @@
     /**
      * Creates a descriptor for values of the given type restricted to the given domain.
      *
-     * @param  <T>          The compile-time type of the {@code valueClass} argument.
-     * @param  valueClass   The class that describe the type of the parameter values.
-     * @param  minimumValue The minimum parameter value (inclusive), or {@code null} if none.
-     * @param  maximumValue The maximum parameter value (inclusive), or {@code null} if none.
-     * @param  defaultValue The default value for the parameter, or {@code null} if none.
-     * @return The parameter descriptor for the given domain of values.
+     * @param  <T>           the compile-time type of the {@code valueClass} argument.
+     * @param  valueClass    the class that describe the type of the parameter values.
+     * @param  minimumValue  the minimum parameter value (inclusive), or {@code null} if none.
+     * @param  maximumValue  the maximum parameter value (inclusive), or {@code null} if none.
+     * @param  defaultValue  the default value for the parameter, or {@code null} if none.
+     * @return the parameter descriptor for the given domain of values.
      */
     @SuppressWarnings({"unchecked", "rawtypes"})
     public <T extends Comparable<? super T>> ParameterDescriptor<T> createBounded(final Class<T> valueClass,
@@ -277,10 +277,10 @@
      * Creates a descriptor for values in the domain represented by the given {@code Range} object.
      * This method allows to specify whether the minimum and maximum values are inclusive or not.
      *
-     * @param  <T>          The type of the parameter values.
-     * @param  valueDomain  The minimum value, maximum value and unit of measurement.
-     * @param  defaultValue The default value for the parameter, or {@code null} if none.
-     * @return The parameter descriptor for the given domain of values.
+     * @param  <T>           the type of the parameter values.
+     * @param  valueDomain   the minimum value, maximum value and unit of measurement.
+     * @param  defaultValue  the default value for the parameter, or {@code null} if none.
+     * @return the parameter descriptor for the given domain of values.
      */
     public <T extends Comparable<? super T>> ParameterDescriptor<T> createBounded(
             final Range<T> valueDomain, final T defaultValue)
@@ -297,12 +297,12 @@
      * a {@linkplain org.opengis.util.CodeList code list} or {@linkplain Enum enumeration} subset.
      * It is not necessary to provide this property when all values from the code list or enumeration are valid.</p>
      *
-     * @param  <T>          The compile-time type of the {@code valueClass} argument.
-     * @param  valueClass   The class that describe the type of the parameter values.
-     * @param  validValues  A finite set of valid values (usually from a code list or enumeration)
-     *                      or {@code null} if it doesn't apply.
-     * @param  defaultValue The default value for the parameter, or {@code null} if none.
-     * @return The parameter descriptor for the given set of valid values.
+     * @param  <T>           the compile-time type of the {@code valueClass} argument.
+     * @param  valueClass    the class that describe the type of the parameter values.
+     * @param  validValues   a finite set of valid values (usually from a code list or enumeration)
+     *                       or {@code null} if it doesn't apply.
+     * @param  defaultValue  the default value for the parameter, or {@code null} if none.
+     * @return the parameter descriptor for the given set of valid values.
      */
     public <T> ParameterDescriptor<T> createEnumerated(final Class<T> valueClass, final T[] validValues, final T defaultValue) {
         ensureNonNull("valueClass", valueClass);
@@ -330,13 +330,13 @@
     /**
      * Creates a descriptor group for the given cardinality and parameters.
      *
-     * @param  minimumOccurs The {@linkplain DefaultParameterDescriptorGroup#getMinimumOccurs() minimum}
-     *                       number of times that values for this parameter group are required.
-     * @param  maximumOccurs The {@linkplain DefaultParameterDescriptorGroup#getMaximumOccurs() maximum}
-     *                       number of times that values for this parameter group are required.
-     * @param  parameters    The {@linkplain DefaultParameterDescriptorGroup#descriptors() parameter descriptors}
-     *                       for the group to create.
-     * @return The parameter descriptor group.
+     * @param  minimumOccurs  the {@linkplain DefaultParameterDescriptorGroup#getMinimumOccurs() minimum}
+     *                        number of times that values for this parameter group are required.
+     * @param  maximumOccurs  the {@linkplain DefaultParameterDescriptorGroup#getMaximumOccurs() maximum}
+     *                        number of times that values for this parameter group are required.
+     * @param  parameters     the {@linkplain DefaultParameterDescriptorGroup#descriptors() parameter descriptors}
+     *                        for the group to create.
+     * @return the parameter descriptor group.
      */
     public ParameterDescriptorGroup createGroup(final int minimumOccurs, final int maximumOccurs,
             final GeneralParameterDescriptor... parameters)
@@ -356,9 +356,9 @@
      * {@link #createGroup(int, int, GeneralParameterDescriptor[])} with a cardinality of [0 … 1]
      * or [1 … 1] depending on the value given to the last call to {@link #setRequired(boolean)}.
      *
-     * @param  parameters The {@linkplain DefaultParameterDescriptorGroup#descriptors() parameter descriptors}
+     * @param  parameters  the {@linkplain DefaultParameterDescriptorGroup#descriptors() parameter descriptors}
      *         for the group to create.
-     * @return The parameter descriptor group.
+     * @return the parameter descriptor group.
      */
     public ParameterDescriptorGroup createGroup(final GeneralParameterDescriptor... parameters) {
         return createGroup(required ? 1 : 0, 1, parameters);
@@ -373,8 +373,8 @@
      * expect the same parameters than their <cite>"Position Vector transformation"</cite> counterpart
      * (EPSG codes 1033, 1037 and 9606) but perform the rotation in the opposite direction.</div>
      *
-     * @param parameters The existing group from which to copy the parameters.
-     * @return The parameter descriptor group.
+     * @param  parameters  the existing group from which to copy the parameters.
+     * @return the parameter descriptor group.
      *
      * @since 0.7
      */
@@ -421,9 +421,9 @@
      * minimum} and {@linkplain DefaultParameterDescriptorGroup#getMaximumOccurs() maximum occurrence} of 1,
      * regardless the value given to {@link #setRequired(boolean)}.
      *
-     * @param  parameters The {@linkplain DefaultParameterDescriptorGroup#descriptors() parameter descriptors}
+     * @param  parameters the {@linkplain DefaultParameterDescriptorGroup#descriptors() parameter descriptors}
      *         for the group to create.
-     * @return The parameter descriptor group for a map projection.
+     * @return the parameter descriptor group for a map projection.
      *
      * @since 0.6
      */
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultCoordinateOperationFactory.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultCoordinateOperationFactory.java
index f9c0f05..f2fc6fb 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultCoordinateOperationFactory.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultCoordinateOperationFactory.java
@@ -567,7 +567,7 @@
          * from ISO 19111 since 'SingleOperation' is conceptually abstract.  But we do that as a way to said that
          * we are missing this important piece of information but still go ahead.
          *
-         * It is unconvenient to guarantee that the created operation is an instance of 'baseType' since the user
+         * It is inconvenient to guarantee that the created operation is an instance of 'baseType' since the user
          * could have specified an implementation class or a custom sub-interface. We will perform the type check
          * only after object creation.
          */
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/ConformalProjection.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/ConformalProjection.java
index 2441617..374ef16 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/ConformalProjection.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/ConformalProjection.java
@@ -167,10 +167,10 @@
          * For each line below, add the smallest values first in order to reduce rounding errors.
          * The smallest values are the one using the eccentricity raised to the highest power.
          */
-        ci2  =    13/   360.* e8  +   1/ 12.* e6  +  5/24.* e4  +  e2/2;
-        ci4  =   811/ 11520.* e8  +  29/240.* e6  +  7/48.* e4;
-        ci6  =    81/  1120.* e8  +   7/120.* e6;
-        ci8  =  4279/161280.* e8;
+        ci2  =    13/   360. * e8  +   1/ 12. * e6  +  5/24. * e4  +  e2/2;
+        ci4  =   811/ 11520. * e8  +  29/240. * e6  +  7/48. * e4;
+        ci6  =    81/  1120. * e8  +   7/120. * e6;
+        ci8  =  4279/161280. * e8;
         /*
          * When rewriting equations using trigonometric identities, some constants appear.
          * For example sin(2θ) = 2⋅sinθ⋅cosθ, so we can factor out the 2 constant into the
@@ -257,7 +257,7 @@
                + ci2 * sin(2*φ);
         } else {
             /*
-             * Same formula than above, be rewriten using trigonometric identities in order to have only two
+             * Same formula than above, but rewriten using trigonometric identities in order to have only two
              * calls to Math.sin/cos instead than 5. The performance gain is twice faster on tested machine.
              */
             final double sin_2φ = sin(2*φ);
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
new file mode 100644
index 0000000..ef5c624
--- /dev/null
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/CylindricalEqualArea.java
@@ -0,0 +1,375 @@
+/*
+ * 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.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.opengis.referencing.operation.TransformException;
+import org.opengis.util.FactoryException;
+import org.apache.sis.internal.referencing.provider.Mercator1SP;
+import org.apache.sis.internal.referencing.provider.LambertCylindricalEqualAreaSpherical;
+import org.apache.sis.internal.util.DoubleDouble;
+import org.apache.sis.parameter.Parameters;
+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 org.apache.sis.util.Workaround;
+
+import static java.lang.Math.*;
+import static org.apache.sis.internal.util.DoubleDouble.verbatim;
+import static org.apache.sis.internal.referencing.provider.LambertCylindricalEqualArea.*;
+
+
+/**
+ * <cite>Cylindrical Equal Area</cite> projection (EPSG codes 9834, 9835).
+ * This is the simplest equal-area projection.
+ * This projection has various names depending on its standard parallel:
+ *
+ * <table class="sis">
+ *   <caption>Non-exhaustive list of variants</caption>
+ *   <tr><th>Name</th>                              <th>Standard parallel</th></tr>
+ *   <tr><td>Lambert cylindrical equal-area</td>    <td>0°</td></tr>
+ *   <tr><td>Behrmann cylindrical equal-area</td>   <td>30°</td></tr>
+ *   <tr><td>Gall orthographic</td>                 <td>45°</td></tr>
+ *   <tr><td>Balthasart</td>                        <td>50°</td></tr>
+ * </table>
+ *
+ * <div class="section">Description</div>
+ * The parallels and the meridians are straight lines and cross at right angles.
+ * The scale is true along standard parallels, but distortion increase greatly at other locations.
+ * Distortions are so great that there is little use of this projection for world mapping purposes.
+ * However this projection may be useful for computing areas.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @since   0.8
+ * @version 0.8
+ * @module
+ */
+public class CylindricalEqualArea extends EqualAreaProjection {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = 8840395516658904421L;
+
+    /**
+     * Returns the variant of the projection based on the name and identifier of the given operation method.
+     * See {@link #variant} for the list of possible values.
+     */
+    private static byte getVariant(final OperationMethod method) {
+        if (identMatch(method, "(?i).*\\bSpherical\\b.*", LambertCylindricalEqualAreaSpherical.IDENTIFIER)) {
+            return Initializer.AUTHALIC_RADIUS;
+        }
+        return 0;
+    }
+
+    /**
+     * The type of Cylindrical Equal Area projection. Possible values are:
+     *
+     * <ul>
+     *   <li>0 if this projection is a default variant.</li>
+     *   <li>{@link Initializer#AUTHALIC_RADIUS} if this projection is the "Lambert Cylindrical Equal Area (Spherical)"
+     *       case, in which case the semi-major and semi-minor axis lengths should be replaced by the authalic radius
+     *       (this replacement is performed by the {@link Initializer} constructor).</li>
+     * </ul>
+     *
+     * Other cases may be added in the future.
+     *
+     * @see #getVariant(OperationMethod)
+     */
+    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.
+     * @param parameters The parameter values of the projection to create.
+     */
+    public CylindricalEqualArea(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);
+        /*
+         * "Longitude of origin" and "scale factor" are intentionally omitted from this map because they will
+         * be handled in a special way. See comments in Mercator.initializer(…) method for more details.
+         */
+        roles.put(ParameterRole.FALSE_EASTING,  FALSE_EASTING);
+        roles.put(ParameterRole.FALSE_NORTHING, FALSE_NORTHING);
+        return new Initializer(method, parameters, roles, getVariant(method));
+    }
+
+    /**
+     * 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 CylindricalEqualArea(final Initializer initializer) {
+        super(initializer);
+        variant = initializer.variant;
+        final MatrixSIS denormalize = context.getMatrix(ContextualParameters.MatrixRole.DENORMALIZATION);
+        /*
+         * The longitude of origin is normally subtracted in the 'normalize' matrix. But in the particular of case
+         * of this map projection we can apply -λ₀ on any matrix.  So we apply that operation on 'denormalize' for
+         * consistency with the Mercator projection and for increasing the chances to have cancellation when
+         * multiplying matrices together.
+         */
+        final double λ0 = initializer.getAndStore(LONGITUDE_OF_ORIGIN);
+        if (λ0 != 0) {
+            final DoubleDouble offset = DoubleDouble.createDegreesToRadians();
+            offset.multiply(-λ0);
+            denormalize.convertBefore(0, null, offset);
+        }
+        /*
+         * Compute the scale factor as k₀ = cosφ₁/√(1 - ℯ²⋅sin²φ₁), multiplied by user-specified scale factor if any.
+         * Explicit scale factor is not formally a Cylindrical Equal Area parameter (it is rather computed from φ₁),
+         * but we nevertheless support it.
+         */
+        final double φ1 = toRadians(initializer.getAndStore(STANDARD_PARALLEL));
+        final DoubleDouble k0 = verbatim(initializer.scaleAtφ(sin(φ1), cos(φ1)));
+        k0.multiply(initializer.getAndStore(Mercator1SP.SCALE_FACTOR));
+        /*
+         * In most Apache SIS map projection implementations, the scale factor is handled by the super-class by
+         * specifying a ParameterRole.SCALE_FACTOR. However in the case of this CylindricalEqualArea we rather
+         * handle the scale factor ourselves, because we do not perform the same multiplication on both axes:
+         *
+         *      x shall be multiplied by k₀
+         *      y shall be divided by k₀
+         *
+         * Furthermore we also multiply y by (1-ℯ²)/2 for avoiding the need to recompute this constant during
+         * the projection of every point.
+         */
+        final DoubleDouble ik = new DoubleDouble(1, 0);
+        ik.subtract(initializer.eccentricitySquared);
+        ik.multiply(0.5, 0);                 // This line need to be cancelled when using spherical formulas.
+        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);
+    }
+
+    /**
+     * Creates a new projection initialized to the same parameters than the given one.
+     */
+    CylindricalEqualArea(final CylindricalEqualArea other) {
+        super(other);
+        variant = other.variant;
+        qmPolar = other.qmPolar;
+    }
+
+    /**
+     * 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 {
+        CylindricalEqualArea kernel = this;
+        if (variant == Initializer.AUTHALIC_RADIUS || 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}
+     * (linear distance on a unit sphere). 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+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.
+        }
+        /*
+         * End of map projection. Now compute the derivative, if requested.
+         */
+        return derivate ? new Matrix2(1, 0, 0, dqm_dφ(sinφ, cos(φ))) : null;
+    }
+
+    /**
+     * Converts a list of coordinate point ordinal values.
+     *
+     * <div class="note"><b>Note:</b>
+     * We override the super-class method only as an optimization in the special case where the target coordinates
+     * are written at the same locations than the source coordinates. In such case, we can take advantage of the
+     * fact that the λ values are not modified by the normalized Cylindrical Equal Area projection.</div>
+     *
+     * @throws TransformException if a point can not be converted.
+     */
+    @Override
+    public void transform(final double[] srcPts, int srcOff,
+                          final double[] dstPts, int dstOff, int numPts)
+            throws TransformException
+    {
+        if (srcPts != dstPts || srcOff != dstOff) {
+            super.transform(srcPts, srcOff, dstPts, dstOff, numPts);
+        } else {
+            dstOff--;
+            while (--numPts >= 0) {
+                final double φ = dstPts[dstOff += 2];                   // Same as srcPts[srcOff + 1].
+                dstPts[dstOff] = qm(sin(φ));                            // Part of Synder equation (10-15)
+            }
+        }
+    }
+
+    /**
+     * Converts the specified (<var>x</var>,<var>y</var>) coordinates
+     * and stores the result in {@code dstPts} (angles in radians).
+     *
+     * @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 y   = srcPts[srcOff+1];            // Must be before writing x.
+        dstPts[dstOff  ] = srcPts[srcOff  ];            // Must be before writing y.
+        dstPts[dstOff+1] = φ(y / qmPolar);
+        /*
+         * Equation 10-26 of Synder gives β = asin(2y⋅k₀/(a⋅qPolar)).
+         * In our case it simplifies to sinβ = (y/qmPolar) because:
+         *
+         *   - y is already multiplied by 2k₀/a because of the denormalization matrix
+         *   - the missing (1-ℯ²) term in qmPolar (compared to qPolar) is in the denormalization matrix.
+         *   - taking the arc sine of β is left to φ(double) function.
+         */
+    }
+
+
+    /**
+     * Provides the transform equations for the spherical case of the Cylindrical Equal Area projection.
+     *
+     * @author  Martin Desruisseaux (Geomatys)
+     * @since   0.8
+     * @version 0.8
+     * @module
+     */
+    static final class Spherical extends CylindricalEqualArea {
+        /**
+         * For cross-version compatibility.
+         */
+        private static final long serialVersionUID = 1063449347697947732L;
+
+        /**
+         * 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.
+         */
+        Spherical(final CylindricalEqualArea other) {
+            super(other);
+            context.getMatrix(ContextualParameters.MatrixRole.DENORMALIZATION).convertAfter(1, 2, null);
+        }
+
+        /**
+         * {@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+1];
+            if (dstPts != null) {
+                dstPts[dstOff  ] = srcPts[srcOff];
+                dstPts[dstOff+1] = sin(φ);
+            }
+            return derivate ? new Matrix2(1, 0, 0, cos(φ)) : null;
+        }
+
+        /**
+         * {@inheritDoc}
+         *
+         * <div class="note"><b>Note:</b>
+         * This method must be overridden because the {@link Mercator} class overrides the {@link NormalizedProjection}
+         * default implementation.</div>
+         */
+        @Override
+        public void transform(final double[] srcPts, int srcOff,
+                              final double[] dstPts, int dstOff, int numPts)
+                throws TransformException
+        {
+            if (srcPts != dstPts || srcOff != dstOff) {
+                super.transform(srcPts, srcOff, dstPts, dstOff, numPts);
+            } else {
+                dstOff--;
+                while (--numPts >= 0) {
+                    final double φ = dstPts[dstOff += 2];           // Same as srcPts[srcOff + 1].
+                    dstPts[dstOff] = sin(φ);
+                }
+            }
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        protected void inverseTransform(final double[] srcPts, final int srcOff,
+                                        final double[] dstPts, final int dstOff)
+                throws ProjectionException
+        {
+            final double y = srcPts[srcOff+1];                      // Must be before writing x.
+            dstPts[dstOff  ] = srcPts[srcOff];                      // Must be before writing y.
+            dstPts[dstOff+1] = asin(y);
+        }
+    }
+}
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
new file mode 100644
index 0000000..d6a101c
--- /dev/null
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/EqualAreaProjection.java
@@ -0,0 +1,209 @@
+/*
+ * 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.io.IOException;
+import java.io.ObjectInputStream;
+
+import static java.lang.Math.*;
+import static org.apache.sis.math.MathFunctions.atanh;
+
+
+/**
+ * Provides formulas common to Equal Area projections.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @since   0.8
+ * @version 0.8
+ * @module
+ */
+abstract class EqualAreaProjection extends NormalizedProjection {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = -6175270149094989517L;
+
+    /**
+     * {@code false} for using the original formulas as published by Synder, or {@code true} for using formulas
+     * modified using trigonometric identities. The use of trigonometric identities is for reducing the amount
+     * of calls to the {@link Math#sin(double)} and similar methods. Some identities used are:
+     *
+     * <ul>
+     *   <li>sin(2β) = 2⋅sinβ⋅cosβ</li>
+     *   <li>sin(4β) = (2 - 4⋅sin²β)⋅sin(2β)</li>
+     *   <li>sin(8β) = 4⋅sin(2β)⋅(cos²β - sin²β)⋅(8⋅cos⁴β - 8⋅cos²β + 1)</li>
+     * </ul>
+     *
+     * 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).
+     */
+    private static final boolean ALLOW_TRIGONOMETRIC_IDENTITIES = false;
+
+    /**
+     * Coefficients in the series expansion of the inverse projection,
+     * depending only on {@linkplain #eccentricity eccentricity} value.
+     * The series expansion is of the following form:
+     *
+     *     <blockquote>φ = ci₂⋅sin(2β) + ci₄⋅sin(4β) + ci₈⋅sin(8β)</blockquote>
+     *
+     * This {@code EqualAreaProjection} class uses those coefficients in {@link #φ(double)}.
+     *
+     * <p><strong>Consider those fields as final!</strong> They are not final only for sub-class
+     * constructors convenience and for the purpose of {@link #readObject(ObjectInputStream)}.</p>
+     *
+     * @see #computeCoefficients()
+     */
+    private transient double ci2, ci4, ci8;
+
+    /**
+     * Creates a new normalized projection from the parameters computed by the given initializer.
+     *
+     * @param initializer The initializer for computing map projection internal parameters.
+     */
+    EqualAreaProjection(final Initializer initializer) {
+        super(initializer);
+    }
+
+    /**
+     * Computes the coefficients in the series expansions from the {@link #eccentricitySquared} value.
+     * This method shall be invoked after {@code EqualAreaProjection} construction or deserialization.
+     */
+    void computeCoefficients() {
+        final double e2 = eccentricitySquared;
+        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;
+        /*
+         * When rewriting equations using trigonometric identities, some constants appear.
+         * For example sin(2β) = 2⋅sinβ⋅cosβ, so we can factor out the 2 constant into the
+         * into the corresponding 'c' field.
+         */
+        if (ALLOW_TRIGONOMETRIC_IDENTITIES) {
+            // Multiplication by powers of 2 does not bring any additional rounding error.
+            ci2 *=  2;
+            ci4 *=  8;
+            ci8 *= 64;
+        }
+    }
+
+    /**
+     * Creates a new projection initialized to the values of the given one. This constructor may be invoked after
+     * we determined that the default implementation can be replaced by an other one, for example using spherical
+     * formulas instead than the ellipsoidal ones. This constructor allows to transfer all parameters to the new
+     * instance without recomputing them.
+     */
+    EqualAreaProjection(final EqualAreaProjection other) {
+        super(other);
+        ci2 = other.ci2;
+        ci4 = other.ci4;
+        ci8 = other.ci8;
+    }
+
+    /**
+     * Calculates <strong>part</strong> of <var>q</var> from Snyder equation (3-12).
+     * In order to get the <var>q</var> function, this method output must be multiplied
+     * by <code>(1 - {@linkplain #eccentricitySquared})</code>.
+     *
+     * <p>This equation has the following properties:</p>
+     *
+     * <ul>
+     *   <li>Input in the [-1 … +1] range</li>
+     *   <li>Output multiplied by {@code (1 - ℯ²)} in the [-2 … +2] range</li>
+     *   <li>Output of the same sign than input</li>
+     *   <li>q(-sinφ) = -q(sinφ)</li>
+     *   <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.
+     *
+     * @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φ) {
+        final double ℯsinφ = eccentricity * sinφ;
+        return sinφ / (1 - ℯsinφ*ℯsinφ) + atanh(ℯsinφ) / eccentricity;
+    }
+
+    /**
+     * Gets the derivative of the {@link #qm(double)} method.
+     * Callers must multiply the returned value by <code>(1 - {@linkplain #eccentricitySquared})</code>
+     * in order to get the derivative of Snyder equation (3-12).
+     *
+     * @param  sinφ  the sine of latitude.
+     * @param  cosφ  the cosines of latitude.
+     * @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)));
+    }
+
+    /**
+     * Computes the latitude using equation 3-18 from Synder.
+     *
+     * @param  sinβ see Synder equation 10-26.
+     * @return the latitude in radians.
+     */
+    final double φ(final double sinβ) {
+        final double β = asin(sinβ);
+        if (!ALLOW_TRIGONOMETRIC_IDENTITIES) {
+            return 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
+             * multiple calls to sin(double) method. The cost is only one sqrt(double) method call.
+             */
+            final double sin2_β = sinβ*sinβ;                                        // = sin²β
+            final double cos2_β = 1 - sin2_β;                                       // = cos²β
+            final double t2β = sinβ * sqrt(cos2_β);                                 // = sin(2β) /   2
+            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β));
+
+            return (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));
+    }
+
+    /**
+     * Restores transient fields after deserialization.
+     */
+    private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
+        in.defaultReadObject();
+        computeCoefficients();
+    }
+}
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 b9862a0..237afba 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
@@ -20,6 +20,7 @@
 import org.opengis.parameter.ParameterDescriptor;
 import org.opengis.parameter.ParameterNotFoundException;
 import org.opengis.referencing.operation.OperationMethod;
+import org.apache.sis.internal.referencing.Formulas;
 import org.apache.sis.internal.referencing.provider.MapProjection;
 import org.apache.sis.internal.util.Constants;
 import org.apache.sis.parameter.Parameters;
@@ -88,19 +89,33 @@
     final DoubleDouble eccentricitySquared;
 
     /**
-     * Map projection variant. This is a convenience field left at
-     * the discretion of {@link NormalizedProjection} subclasses.
+     * Map projection variant.
+     * Values from 0 to 127 inclusive are convenience values at the discretion of {@link NormalizedProjection} subclasses.
+     * Values from 128 to 255 inclusive are values handled in a special way by {@link Initializer} constructor.
      */
     final byte variant;
 
     /**
+     * A {@link #variant} value telling the constructor to computing the authalic radius instead than using
+     * the semi-major and semi-minor axis lengths directly.
+     *
+     * <p>Note that this value is not necessarily equivalent to the {@code SPHERICAL} value defined in some
+     * map projection, because EPSG guidance notes recommend different approaches for spherical implementations.
+     * For example the Mercator projection will use the radius of conformal sphere instead than the authalic radius.</p>
+     */
+    static final byte AUTHALIC_RADIUS = (byte) 128;
+
+    /**
      * Creates a new initializer. The parameters are described in
      * {@link NormalizedProjection#NormalizedProjection(OperationMethod, Parameters, Map)}.
      *
-     * @param method     Description of the map projection parameters.
-     * @param parameters The parameters of the projection to be created.
-     * @param roles Parameters to look for <cite>central meridian</cite>, <cite>scale factor</cite>,
-     *        <cite>false easting</cite>, <cite>false northing</cite> and other values.
+     * @param method      description of the map projection parameters.
+     * @param parameters  the parameters of the projection to be created.
+     * @param roles       parameters to look for <cite>central meridian</cite>, <cite>scale factor</cite>,
+     *                    <cite>false easting</cite>, <cite>false northing</cite> and other values.
+     * @param variant     convenience field left at the discretion of {@link NormalizedProjection} subclasses.
+     *                    Values equal to greater than 128 are special values recognized by this constructor
+     *                    (see {@link #AUTHALIC_RADIUS}).
      */
     Initializer(final OperationMethod method, final Parameters parameters,
             final Map<ParameterRole, ? extends ParameterDescriptor<? extends Number>> roles,
@@ -114,8 +129,7 @@
         this.variant    = variant;
         /*
          * Note: we do not use Map.getOrDefault(K,V) below because the user could have explicitly associated
-         * a null value to keys (we are paranoiac...) and because it conflicts with the "? extends" part of
-         * in this constructor signature.
+         * a null value to keys (we are paranoiac...) and because it conflicts with the "? extends" parts.
          */
         ParameterDescriptor<? extends Number> semiMajor = roles.get(ParameterRole.SEMI_MAJOR);
         ParameterDescriptor<? extends Number> semiMinor = roles.get(ParameterRole.SEMI_MINOR);
@@ -133,65 +147,70 @@
         eccentricitySquared = new DoubleDouble();
         DoubleDouble k = new DoubleDouble(a);  // The value by which to multiply all results of normalized projection.
         if (a != b) {
-            /*
-             * (1) Using axis lengths:  ℯ² = 1 - (b/a)²
-             * (2) Using flattening;    ℯ² = 2f - f²     where f is the (NOT inverse) flattening factor.
-             *
-             * If the inverse flattening factor is the definitive factor for the ellipsoid, we use (2).
-             * Otherwise use (1). With double-double arithmetic, this makes a difference in the 3 last
-             * digits for the WGS84 ellipsoid.
-             */
-            boolean isIvfDefinitive;
-            try {
-                isIvfDefinitive = parameters.parameter(Constants.IS_IVF_DEFINITIVE).booleanValue();
-            } catch (ParameterNotFoundException e) {
-                /*
-                 * Should never happen with Apache SIS implementation, but may happen if the given parameters come
-                 * from another implementation. We can safely abandon our attempt to get the inverse flattening value,
-                 * since it was redundant with semi-minor axis length.
-                 */
-                isIvfDefinitive = false;
-            }
-            /*
-             * The ellipsoid parameters (a, b or ivf) are assumed accurate in base 10 rather than in base 2,
-             * because they are defined by authorities. For example the semi-major axis length of the WGS84
-             * ellipsoid is equal to exactly 6378137 metres by definition of that ellipsoid. The DoubleDouble
-             * constructor applies corrections for making those values more accurate in base 10 rather than 2.
-             */
-            if (isIvfDefinitive) {
-                final DoubleDouble f = new DoubleDouble(parameters.parameter(Constants.INVERSE_FLATTENING).doubleValue());
-                f.inverseDivide(1,0);
-                eccentricitySquared.setFrom(f);
-                eccentricitySquared.multiply(2,0);
-                f.square();
-                eccentricitySquared.subtract(f);
+            if (variant == AUTHALIC_RADIUS) {
+                k.value = Formulas.getAuthalicRadius(a, b);
+                k.error = 0;
             } else {
-                final DoubleDouble rs = new DoubleDouble(b);
-                rs.divide(k);    // rs = b/a
-                rs.square();
-                eccentricitySquared.value = 1;
-                eccentricitySquared.subtract(rs);
-            }
-            final ParameterDescriptor<? extends Number> radius = roles.get(ParameterRole.LATITUDE_OF_CONFORMAL_SPHERE_RADIUS);
-            if (radius != null) {
                 /*
-                 * EPSG said: R is the radius of the sphere and will normally be one of the CRS parameters.
-                 * If the figure of the earth used is an ellipsoid rather than a sphere then R should be calculated
-                 * as the radius of the conformal sphere at the projection origin at latitude φ₀ using the formula
-                 * for Rc given in section 1.2, table 3.
+                 * (1) Using axis lengths:  ℯ² = 1 - (b/a)²
+                 * (2) Using flattening;    ℯ² = 2f - f²     where f is the (NOT inverse) flattening factor.
                  *
-                 * Table 3 gives:
-                 * Radius of conformal sphere Rc = a √(1 – ℯ²) / (1 – ℯ²⋅sin²φ)
-                 *
-                 * Using √(1 – ℯ²) = b/a we rewrite as: Rc = b / (1 – ℯ²⋅sin²φ)
-                 *
-                 * Equivalent Java code:
-                 *
-                 *     final double sinφ = sin(toRadians(parameters.doubleValue(radius)));
-                 *     k = b / (1 - eccentricitySquared * (sinφ*sinφ));
+                 * If the inverse flattening factor is the definitive factor for the ellipsoid, we use (2).
+                 * Otherwise use (1). With double-double arithmetic, this makes a difference in the 3 last
+                 * digits for the WGS84 ellipsoid.
                  */
-                k = rν2(sin(toRadians(parameters.doubleValue(radius))));
-                k.inverseDivide(b, 0);
+                boolean isIvfDefinitive;
+                try {
+                    isIvfDefinitive = parameters.parameter(Constants.IS_IVF_DEFINITIVE).booleanValue();
+                } catch (ParameterNotFoundException e) {
+                    /*
+                     * Should never happen with Apache SIS implementation, but may happen if the given parameters come
+                     * from another implementation. We can safely abandon our attempt to get the inverse flattening value,
+                     * since it was redundant with semi-minor axis length.
+                     */
+                    isIvfDefinitive = false;
+                }
+                /*
+                 * The ellipsoid parameters (a, b or ivf) are assumed accurate in base 10 rather than in base 2,
+                 * because they are defined by authorities. For example the semi-major axis length of the WGS84
+                 * ellipsoid is equal to exactly 6378137 metres by definition of that ellipsoid. The DoubleDouble
+                 * constructor applies corrections for making those values more accurate in base 10 rather than 2.
+                 */
+                if (isIvfDefinitive) {
+                    final DoubleDouble f = new DoubleDouble(parameters.parameter(Constants.INVERSE_FLATTENING).doubleValue());
+                    f.inverseDivide(1,0);
+                    eccentricitySquared.setFrom(f);
+                    eccentricitySquared.multiply(2,0);
+                    f.square();
+                    eccentricitySquared.subtract(f);
+                } else {
+                    final DoubleDouble rs = new DoubleDouble(b);
+                    rs.divide(k);    // rs = b/a
+                    rs.square();
+                    eccentricitySquared.value = 1;
+                    eccentricitySquared.subtract(rs);
+                }
+                final ParameterDescriptor<? extends Number> radius = roles.get(ParameterRole.LATITUDE_OF_CONFORMAL_SPHERE_RADIUS);
+                if (radius != null) {
+                    /*
+                     * EPSG said: R is the radius of the sphere and will normally be one of the CRS parameters.
+                     * If the figure of the earth used is an ellipsoid rather than a sphere then R should be calculated
+                     * as the radius of the conformal sphere at the projection origin at latitude φ₀ using the formula
+                     * for Rc given in section 1.2, table 3.
+                     *
+                     * Table 3 gives:
+                     * Radius of conformal sphere Rc = a √(1 – ℯ²) / (1 – ℯ²⋅sin²φ)
+                     *
+                     * Using √(1 – ℯ²) = b/a we rewrite as: Rc = b / (1 – ℯ²⋅sin²φ)
+                     *
+                     * Equivalent Java code:
+                     *
+                     *     final double sinφ = sin(toRadians(parameters.doubleValue(radius)));
+                     *     k = b / (1 - eccentricitySquared * (sinφ*sinφ));
+                     */
+                    k = rν2(sin(toRadians(parameters.doubleValue(radius))));
+                    k.inverseDivide(b, 0);
+                }
             }
         }
         /*
@@ -239,7 +258,7 @@
         final Number defaultValue = descriptor.getDefaultValue();
         if (defaultValue == null || !defaultValue.equals(value)) {
             MapProjection.validate(descriptor, value);
-            context.parameter(descriptor.getName().getCode()).setValue(value);
+            context.getOrCreate(descriptor).setValue(value);
         }
         return value;
     }
@@ -255,7 +274,7 @@
             return defaultValue;
         }
         MapProjection.validate(descriptor, value);
-        context.parameter(descriptor.getName().getCode()).setValue(value);
+        context.getOrCreate(descriptor).setValue(value);
         return value;
     }
 
@@ -291,8 +310,8 @@
      *       (otherwise we get {@link Double#NaN}).</li>
      * </ul>
      *
-     * @param  sinφ The sine of the φ latitude.
-     * @return Reciprocal squared of the radius of curvature of the ellipsoid
+     * @param  sinφ  the sine of the φ latitude.
+     * @return reciprocal squared of the radius of curvature of the ellipsoid
      *         perpendicular to the meridian at latitude φ.
      */
     private DoubleDouble rν2(final double sinφ) {
@@ -321,8 +340,8 @@
      * the use of φ₀ (or φ₁ as relevant to method) for φ is suggested, except if the projection is
      * equal area when the radius of authalic sphere should be used.
      *
-     * @param  sinφ The sine of the φ latitude.
-     * @return Radius of the conformal sphere at latitude φ.
+     * @param  sinφ  the sine of the φ latitude.
+     * @return radius of the conformal sphere at latitude φ.
      */
     final double radiusOfConformalSphere(final double sinφ) {
         final DoubleDouble Rc = verbatim(1);
@@ -340,9 +359,9 @@
      * The result is returned as a {@code double} because the limited precision of {@code sinφ} and {@code cosφ}
      * makes the error term meaningless. We use double-double arithmetic only for intermediate calculation.
      *
-     * @param  sinφ The sine of the φ latitude.
-     * @param  cosφ The cosine of the φ latitude.
-     * @return Scale factor at latitude φ.
+     * @param  sinφ  the sine of the φ latitude.
+     * @param  cosφ  the cosine of the φ latitude.
+     * @return scale factor at latitude φ.
      */
     final double scaleAtφ(final double sinφ, final double cosφ) {
         final DoubleDouble s = rν2(sinφ);
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Mercator.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Mercator.java
index f806c7d..768e3dd 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Mercator.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Mercator.java
@@ -95,8 +95,8 @@
      *
      * @see #getVariant(OperationMethod)
      */
-    private static final byte SPHERICAL = 1, PSEUDO = 3,    // Must be odd and SPHERICAL must be 1.
-                              REGIONAL  = 2, MILLER = 4;    // Must be even.
+    private static final byte SPHERICAL = 1, PSEUDO = 3,            // Must be odd and SPHERICAL must be 1.
+                              REGIONAL  = 2, MILLER = 4;            // Must be even.
 
     /**
      * Returns the variant of the projection based on the name and identifier of the given operation method.
@@ -291,7 +291,7 @@
         if (φ0 == 0 && isPositive(φ1 != 0 ? φ1 : φ0)) {
             final Number reverseSign = verbatim(-1);
             normalize  .convertBefore(1, reverseSign, null);
-            denormalize.convertBefore(1, reverseSign, null);  // Must be before false easting/northing.
+            denormalize.convertBefore(1, reverseSign, null);        // Must be before false easting/northing.
         }
     }
 
@@ -354,7 +354,7 @@
                 // about why we perform explicit checks for the pole cases.
                 final double a = abs(φ);
                 if (a < PI/2) {
-                    y = log(expOfNorthing(φ, eccentricity * sinφ));     // Snyder (7-7)
+                    y = log(expOfNorthing(φ, eccentricity * sinφ));                 // Snyder (7-7)
                 } else if (a <= (PI/2 + ANGULAR_TOLERANCE)) {
                     y = copySign(POSITIVE_INFINITY, φ);
                 } else {
@@ -390,7 +390,7 @@
         } else {
             dstOff--;
             while (--numPts >= 0) {
-                final double φ = dstPts[dstOff += 2]; // Same as srcPts[srcOff + 1].
+                final double φ = dstPts[dstOff += 2];                           // Same as srcPts[srcOff + 1].
                 if (φ != 0) {
                     // See the javadoc of the Spherical inner class for a note
                     // about why we perform explicit checks for the pole cases.
@@ -420,8 +420,8 @@
                                     final double[] dstPts, final int dstOff)
             throws ProjectionException
     {
-        final double y   = srcPts[srcOff+1];    // Must be before writing x.
-        dstPts[dstOff  ] = srcPts[srcOff  ];    // Must be before writing y.
+        final double y   = srcPts[srcOff+1];            // Must be before writing x.
+        dstPts[dstOff  ] = srcPts[srcOff  ];            // Must be before writing y.
         dstPts[dstOff+1] = φ(exp(-y));
     }
 
@@ -488,7 +488,7 @@
                     // See class javadoc for a note about explicit check for poles.
                     final double a = abs(φ);
                     if (a < PI/2) {
-                        y = log(tan(PI/4 + 0.5*φ));    // Part of Snyder (7-2)
+                        y = log(tan(PI/4 + 0.5*φ));                             // Part of Snyder (7-2)
                     } else if (a <= (PI/2 + ANGULAR_TOLERANCE)) {
                         y = copySign(POSITIVE_INFINITY, φ);
                     } else {
@@ -518,13 +518,13 @@
             } else {
                 dstOff--;
                 while (--numPts >= 0) {
-                    final double φ = dstPts[dstOff += 2];   // Same as srcPts[srcOff + 1].
+                    final double φ = dstPts[dstOff += 2];                       // Same as srcPts[srcOff + 1].
                     if (φ != 0) {
                         // See class javadoc for a note about explicit check for poles.
                         final double a = abs(φ);
                         final double y;
                         if (a < PI/2) {
-                            y = log(tan(PI/4 + 0.5*φ));     // Part of Snyder (7-2)
+                            y = log(tan(PI/4 + 0.5*φ));                         // Part of Snyder (7-2)
                         } else if (a <= (PI/2 + ANGULAR_TOLERANCE)) {
                             y = copySign(POSITIVE_INFINITY, φ);
                         } else {
@@ -544,9 +544,9 @@
                                         final double[] dstPts, final int dstOff)
                 throws ProjectionException
         {
-            final double y = srcPts[srcOff+1];           // Must be before writing x.
-            dstPts[dstOff  ] = srcPts[srcOff];           // Must be before writing y.
-            dstPts[dstOff+1] = PI/2 - 2*atan(exp(-y));  // Part of Snyder (7-4);
+            final double y = srcPts[srcOff+1];                      // Must be before writing x.
+            dstPts[dstOff  ] = srcPts[srcOff];                      // Must be before writing y.
+            dstPts[dstOff+1] = PI/2 - 2*atan(exp(-y));              // Part of Snyder (7-4);
         }
     }
 }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/PolarStereographic.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/PolarStereographic.java
index 49910c0..c191d17 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/PolarStereographic.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/PolarStereographic.java
@@ -113,7 +113,6 @@
      * 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 byte variant = getVariant(method);
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/package-info.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/package-info.java
index 980fe2b..fd05f2a 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/package-info.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/package-info.java
@@ -160,7 +160,7 @@
  * @author  Rémi Maréchal (Geomatys)
  * @author  Adrian Custer (Geomatys)
  * @since   0.6
- * @version 0.7
+ * @version 0.8
  * @module
  *
  * @see <a href="http://www.remotesensing.org/geotiff/proj_list">Projections list on RemoteSensing.org</a>
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 b1af88b..e93d3d7 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,6 +27,8 @@
 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
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 bea1cd3..762e2ac 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
@@ -37,7 +37,7 @@
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @since   0.6
- * @version 0.7
+ * @version 0.8
  * @module
  */
 @DependsOn({
@@ -79,6 +79,8 @@
             PseudoMercator.class,
             RegionalMercator.class,
             MillerCylindrical.class,
+            LambertCylindricalEqualArea.class,
+            LambertCylindricalEqualAreaSpherical.class,
             LambertConformal1SP.class,
             LambertConformal2SP.class,
             LambertConformalWest.class,
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
new file mode 100644
index 0000000..ee2f29d
--- /dev/null
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/CylindricalEqualAreaTest.java
@@ -0,0 +1,124 @@
+/*
+ * 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 org.apache.sis.internal.referencing.provider.LambertCylindricalEqualArea;
+import org.apache.sis.internal.referencing.provider.LambertCylindricalEqualAreaSpherical;
+import org.apache.sis.test.DependsOnMethod;
+import org.junit.Test;
+
+import static java.lang.StrictMath.*;
+
+
+/**
+ * Tests the {@link CylindricalEqualArea} class.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @since   0.8
+ * @version 0.8
+ * @module
+ */
+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.
+     *
+     * @throws TransformException if an error occurred while projecting a point.
+     */
+    private void testDerivative() throws TransformException {
+        final double delta = toRadians(100.0 / 60) / 1852;      // Approximatively 100 metres.
+        derivativeDeltas = new double[] {delta, delta};
+        tolerance = 1E-6;                                       // More severe than Formulas.LINEAR_TOLERANCE.
+        verifyDerivative(toRadians(15), toRadians( 30));
+        verifyDerivative(toRadians(10), toRadians(-60));
+    }
+
+    /**
+     * Tests <cite>Lambert Cylindrical Equal Area</cite> projection of a point in the in ellipsoidal case.
+     *
+     * @throws FactoryException if an error occurred while creating the map projection.
+     * @throws TransformException if an error occurred while projecting a point.
+     */
+    @Test
+    public void testEllipsoidal() throws FactoryException, TransformException {
+        createCompleteProjection(true, 0, 0);
+        tolerance = Formulas.LINEAR_TOLERANCE;
+        final double λ = 2;
+        final double φ = 1;
+        final double x = 222638.98;             // Test point from Proj.4.
+        final double y = 110568.81;
+        verifyTransform(new double[] {λ, φ,  -λ, φ,  λ, -φ,  -λ, -φ},
+                        new double[] {x, y,  -x, y,  x, -y,  -x, -y});
+        testDerivative();
+    }
+
+    /**
+     * Tests <cite>Lambert Cylindrical Equal Area</cite> projection of a point in the in spherical case.
+     *
+     * @throws FactoryException if an error occurred while creating the map projection.
+     * @throws TransformException if an error occurred while projecting a point.
+     */
+    @Test
+    @DependsOnMethod("testEllipsoidal")
+    public void testSpherical() throws FactoryException, TransformException {
+        createCompleteProjection(false, 0, 0);
+        tolerance = Formulas.LINEAR_TOLERANCE;
+        final double λ = 2;
+        final double φ = 1;
+        final double x = 222390.10;             // Anti-regression values (not from an external source).
+        final double y = 111189.40;
+        verifyTransform(new double[] {λ, φ,  -λ, φ,  λ, -φ,  -λ, -φ},
+                        new double[] {x, y,  -x, y,  x, -y,  -x, -y});
+        testDerivative();
+    }
+
+    /**
+     * Tests <cite>Lambert Cylindrical Equal Area (Spherical)</cite> projection.
+     * The difference between this test and {@link #testSpherical()} is that this case shall
+     * compute the radius of the conformal sphere instead than using the semi-major axis length.
+     * The result near the equator are almost the same however.
+     *
+     * @throws FactoryException if an error occurred while creating the map projection.
+     * @throws TransformException if an error occurred while projecting a point.
+     */
+    @Test
+    @DependsOnMethod("testSpherical")
+    public void testSphericalWithConformalSphereRadius() throws FactoryException, TransformException {
+        createCompleteProjection(new LambertCylindricalEqualAreaSpherical(), true, 0, 0, 0, 1, 0, 0);
+        tolerance = Formulas.LINEAR_TOLERANCE;
+        final double λ = 2;
+        final double φ = 1;
+        final double x = 222390.10;             // Anti-regression values (not from an external source).
+        final double y = 111189.40;
+        verifyTransform(new double[] {λ, φ,  -λ, φ,  λ, -φ,  -λ, -φ},
+                        new double[] {x, y,  -x, y,  x, -y,  -x, -y});
+        testDerivative();
+    }
+}
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 4c0382c..6132f6c 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
@@ -127,7 +127,7 @@
     /**
      * Tests the projection at some special latitudes (0, ±π/2, NaN).
      *
-     * @throws ProjectionException Should never happen.
+     * @throws ProjectionException if an error occurred while projecting a point.
      */
     @Test
     public void testSpecialLatitudes() throws ProjectionException {
@@ -157,7 +157,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")
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/InterpolatedMolodenskyTransformTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/InterpolatedMolodenskyTransformTest.java
index 517ba92..bbb0cdb 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/InterpolatedMolodenskyTransformTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/InterpolatedMolodenskyTransformTest.java
@@ -115,8 +115,8 @@
                 "  Param_MT[“Molodensky inverse interpolation (radians domain)”,\n" +
                 "    Parameter[“src_semi_major”, 6378249.2],\n" +
                 "    Parameter[“src_semi_minor”, 6356515.0],\n" +
-                "    Parameter[“Semi-major axis length difference”, -112.2],\n" +
-                "    Parameter[“Flattening difference”, -5.4738838833299144E-5],\n" +
+                "    Parameter[“Semi-major axis length difference”, -112.2, Id[“EPSG”, 8654]],\n" +
+                "    Parameter[“Flattening difference”, -5.4738838833299144E-5, Id[“EPSG”, 8655]],\n" +
                 "    ParameterFile[“Geocentric translation file”, “\\E.*\\W\\Q" +
                                    FranceGeocentricInterpolationTest.TEST_FILE + "”, Id[“EPSG”, 8727],\n" +
                 "      Remark[“\\E.*\\Q”]],\n" +
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/MathTransformTestCase.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/MathTransformTestCase.java
index 53f5fe1..bcff6a2 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/MathTransformTestCase.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/MathTransformTestCase.java
@@ -181,7 +181,7 @@
         final float[] asFloats = Numerics.copyAsFloats(coordinates);
         final float[] result   = verifyConsistency(asFloats);
         for (int i=0; i<coordinates.length; i++) {
-            assertEquals("Detected change in source coordinates.", (float) coordinates[i], asFloats[i], 0f); // Paranoiac check.
+            assertEquals("Detected change in source coordinates.", (float) coordinates[i], asFloats[i], 0f);    // Paranoiac check.
         }
         /*
          * The comparison below needs a higher tolerance threshold, because we converted the source
@@ -195,7 +195,7 @@
             for (int i=0; i<expected.length; i++) {
                 final double e = expected[i];
                 double tol = 1E-6 * abs(e);
-                if (!(tol > tolerance)) {   // Use '!' for replacing NaN by 'tolerance'.
+                if (!(tol > tolerance)) {               // Use '!' for replacing NaN by 'tolerance'.
                     tol = tolerance;
                 }
                 assertEquals(e, result[i], tol);
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/MolodenskyTransformTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/MolodenskyTransformTest.java
index 08512aa..84cd22a 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/MolodenskyTransformTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/MolodenskyTransformTest.java
@@ -302,11 +302,11 @@
                 "  Param_MT[“Molodensky (radians domain)”,\n" +
                 "    Parameter[“src_semi_major”, 6378137.0],\n" +
                 "    Parameter[“src_semi_minor”, 6356752.314245179],\n" +
-                "    Parameter[“Semi-major axis length difference”, 251.0],\n" +
-                "    Parameter[“Flattening difference”, 1.4192702255886284E-5],\n" +
-                "    Parameter[“X-axis translation”, 84.87],\n" +
-                "    Parameter[“Y-axis translation”, 96.49],\n" +
-                "    Parameter[“Z-axis translation”, 116.95],\n" +
+                "    Parameter[“Semi-major axis length difference”, 251.0, Id[“EPSG”, 8654]],\n" +
+                "    Parameter[“Flattening difference”, 1.4192702255886284E-5, Id[“EPSG”, 8655]],\n" +
+                "    Parameter[“X-axis translation”, 84.87, Id[“EPSG”, 8605]],\n" +
+                "    Parameter[“Y-axis translation”, 96.49, Id[“EPSG”, 8606]],\n" +
+                "    Parameter[“Z-axis translation”, 116.95, Id[“EPSG”, 8607]],\n" +
                 "    Parameter[“abridged”, TRUE],\n" +
                 "    Parameter[“dim”, 3]],\n" +
                 "  Param_MT[“Affine”,\n" +
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/report/CoordinateOperationMethods.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/report/CoordinateOperationMethods.java
index 4595812..e4b8f43 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/report/CoordinateOperationMethods.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/report/CoordinateOperationMethods.java
@@ -66,7 +66,7 @@
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @since   0.6
- * @version 0.7
+ * @version 0.8
  * @module
  */
 public strictfp class CoordinateOperationMethods extends HTMLGenerator {
@@ -165,7 +165,7 @@
      * @throws IOException if an error occurred while writing to the file.
      */
     public CoordinateOperationMethods() throws IOException {
-        super("CoordinateOperationMethods.html", "Apache SIS Coordinate Operation Methods");
+        super("CoordinateOperationMethods.html", "Apache SIS Coordinate Operation Methods", "authority-codes.css");
         domainOfValidity = Collections.emptyMap();      // TODO: not yet available.
         rangeFormat = new RangeFormat(LOCALE);
         final int header = openTag("header");
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/report/HTMLGenerator.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/report/HTMLGenerator.java
index 995a8a1..853088f 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/report/HTMLGenerator.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/report/HTMLGenerator.java
@@ -83,11 +83,12 @@
      * Creates a new instance which will write in the given file.
      * This constructor immediately writes the HTML header up to the {@code <body>} line, inclusive.
      *
-     * @param  filename The name of the file where to write.
-     * @param  title The document title.
+     * @param  filename  the name of the file where to write.
+     * @param  title     the document title.
+     * @param  path      path to the CSS file.
      * @throws IOException if the file can not be created (e.g. because it already exists).
      */
-    HTMLGenerator(final String filename, final String title) throws IOException {
+    HTMLGenerator(final String filename, final String title, final String css) throws IOException {
         final File file = new File(filename);
         if (file.exists()) {
             throw new IOException("File " + file.getAbsolutePath() + " already exists.");
@@ -112,7 +113,11 @@
         out.newLine();
         println("title", title);
         openTag("style type=\"text/css\" media=\"all\"");
-        println("@import url(\"../tables.css\");");
+        out.write(margin);
+        out.write("@import url(\"");
+        out.write(css);
+        out.write("\");");
+        out.newLine();
         closeTags(head);
         openTag("body");
     }
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/report/package-info.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/report/package-info.java
index 56f7baa..7a3b7b2 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/report/package-info.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/report/package-info.java
@@ -23,7 +23,7 @@
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @since   0.7
- * @version 0.7
+ * @version 0.8
  * @module
  */
 package org.apache.sis.referencing.report;
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 03170ad..804991c 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
@@ -164,6 +164,7 @@
     org.apache.sis.referencing.operation.projection.TransverseMercatorTest.class,
     org.apache.sis.referencing.operation.projection.PolarStereographicTest.class,
     org.apache.sis.referencing.operation.projection.ObliqueStereographicTest.class,
+    org.apache.sis.referencing.operation.projection.CylindricalEqualAreaTest.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/internal/jdk8/UncheckedIOException.java b/core/sis-utility/src/main/java/org/apache/sis/internal/jdk8/UncheckedIOException.java
new file mode 100644
index 0000000..8f078e0
--- /dev/null
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/jdk8/UncheckedIOException.java
@@ -0,0 +1,45 @@
+/*
+ * 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.jdk8;
+
+import java.io.IOException;
+
+
+/**
+ * Placeholder for the {@link java.io.UncheckedIOException}.
+ */
+@SuppressWarnings("serial")
+public class UncheckedIOException extends RuntimeException {
+    /**
+     * Wraps the given I/O exception.
+     *
+     * @param e the I/O exception to wrap.
+     */
+    public UncheckedIOException(IOException e) {
+        super(e);
+    }
+
+    /**
+     * Returns the I/O exception.
+     *
+     * @return the I/O exception specified at construction time.
+     */
+    @Override
+    public IOException getCause() {
+        return (IOException) super.getCause();
+    }
+}
diff --git a/core/sis-utility/src/main/java/org/apache/sis/math/MathFunctions.java b/core/sis-utility/src/main/java/org/apache/sis/math/MathFunctions.java
index 728fba3..b4b9298 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/math/MathFunctions.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/math/MathFunctions.java
@@ -364,6 +364,8 @@
         /*
          * The classical formulas is log((1+x)/(1-x))/2, but the following is more
          * accurate if the (1+x)/(1-x) ratio is close to 1, i.e. if x is close to 0.
+         * This is often the case in Apache SIS since x is often a value close to the
+         * Earth excentricity, which is a small value (0 would be a perfect sphere).
          */
         return 0.5 * Math.log1p(2*x / (1-x));
     }
diff --git a/ide-project/NetBeans/build.xml b/ide-project/NetBeans/build.xml
index e8c1300..aafab68 100644
--- a/ide-project/NetBeans/build.xml
+++ b/ide-project/NetBeans/build.xml
@@ -191,16 +191,12 @@
         <include name="**/*.laa"/>
         <include name="**/*.loa"/>
       </fileset>
-    </copy>
-    <copy todir="${build.test.classes.dir}">
       <fileset dir="${project.root}/storage/sis-shapefile/src/test/resources">
         <include name="**/*.dbf"/>
         <include name="**/*.prj"/>
         <include name="**/*.shp"/>
         <include name="**/*.shx"/>
       </fileset>
-    </copy>
-    <copy todir="${build.test.classes.dir}">
       <fileset dir="${project.root}/profiles/sis-french-profile/src/test/resources">
         <include name="**/*.xml"/>
       </fileset>