sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject svn commit: r1799452 [6/7] - in /sis/branches/JDK7: ./ core/sis-build-helper/src/main/java/org/apache/sis/internal/doclet/ core/sis-feature/src/main/java/org/apache/sis/feature/ core/sis-feature/src/test/java/org/apache/sis/feature/ core/sis-feature/sr...
Date Wed, 21 Jun 2017 14:11:41 GMT
Modified: sis/branches/JDK7/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK7/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java?rev=1799452&r1=1799451&r2=1799452&view=diff
==============================================================================
--- sis/branches/JDK7/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java [UTF-8] (original)
+++ sis/branches/JDK7/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java [UTF-8] Wed Jun 21 14:11:39 2017
@@ -18,27 +18,38 @@ package org.apache.sis.internal.storage;
 
 import java.util.Date;
 import java.util.Locale;
+import java.util.Iterator;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.nio.charset.Charset;
+import javax.measure.Unit;
+import org.opengis.util.MemberName;
 import org.opengis.util.GenericName;
 import org.opengis.util.InternationalString;
 import org.opengis.metadata.Identifier;
 import org.opengis.metadata.citation.Role;
 import org.opengis.metadata.citation.DateType;
+import org.opengis.metadata.citation.Citation;
+import org.opengis.metadata.citation.CitationDate;
+import org.opengis.metadata.citation.Responsibility;
 import org.opengis.metadata.spatial.Dimension;
 import org.opengis.metadata.spatial.DimensionNameType;
 import org.opengis.metadata.spatial.CellGeometry;
 import org.opengis.metadata.spatial.PixelOrientation;
+import org.opengis.metadata.spatial.SpatialRepresentationType;
 import org.opengis.metadata.constraint.Restriction;
+import org.opengis.metadata.content.TransferFunctionType;
 import org.opengis.metadata.maintenance.ScopeCode;
 import org.opengis.metadata.acquisition.Context;
 import org.opengis.metadata.acquisition.OperationType;
 import org.opengis.metadata.identification.Progress;
+import org.opengis.metadata.identification.KeywordType;
+import org.opengis.metadata.identification.TopicCategory;
 import org.opengis.metadata.distribution.Format;
+import org.opengis.referencing.crs.VerticalCRS;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.geometry.AbstractEnvelope;
@@ -46,14 +57,19 @@ import org.apache.sis.metadata.iso.Defau
 import org.apache.sis.metadata.iso.DefaultIdentifier;
 import org.apache.sis.metadata.iso.DefaultMetadataScope;
 import org.apache.sis.metadata.iso.extent.DefaultExtent;
+import org.apache.sis.metadata.iso.extent.DefaultVerticalExtent;
 import org.apache.sis.metadata.iso.extent.DefaultTemporalExtent;
 import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
+import org.apache.sis.metadata.iso.extent.DefaultGeographicDescription;
+import org.apache.sis.metadata.iso.spatial.DefaultGridSpatialRepresentation;
 import org.apache.sis.metadata.iso.spatial.DefaultDimension;
 import org.apache.sis.metadata.iso.spatial.DefaultGeorectified;
+import org.apache.sis.metadata.iso.spatial.DefaultGeoreferenceable;
 import org.apache.sis.metadata.iso.content.DefaultAttributeGroup;
 import org.apache.sis.metadata.iso.content.DefaultSampleDimension;
 import org.apache.sis.metadata.iso.content.DefaultCoverageDescription;
 import org.apache.sis.metadata.iso.content.DefaultFeatureCatalogueDescription;
+import org.apache.sis.metadata.iso.content.DefaultRangeElementDescription;
 import org.apache.sis.metadata.iso.content.DefaultImageDescription;
 import org.apache.sis.metadata.iso.content.DefaultFeatureTypeInfo;
 import org.apache.sis.metadata.iso.citation.AbstractParty;
@@ -63,9 +79,12 @@ import org.apache.sis.metadata.iso.citat
 import org.apache.sis.metadata.iso.citation.DefaultIndividual;
 import org.apache.sis.metadata.iso.citation.DefaultOrganisation;
 import org.apache.sis.metadata.iso.constraint.DefaultLegalConstraints;
+import org.apache.sis.metadata.iso.identification.DefaultKeywords;
 import org.apache.sis.metadata.iso.identification.DefaultResolution;
 import org.apache.sis.metadata.iso.identification.DefaultDataIdentification;
 import org.apache.sis.metadata.iso.distribution.DefaultFormat;
+import org.apache.sis.metadata.iso.distribution.DefaultDistributor;
+import org.apache.sis.metadata.iso.distribution.DefaultDistribution;
 import org.apache.sis.metadata.iso.acquisition.DefaultAcquisitionInformation;
 import org.apache.sis.metadata.iso.acquisition.DefaultEvent;
 import org.apache.sis.metadata.iso.acquisition.DefaultInstrument;
@@ -92,7 +111,7 @@ import org.opengis.feature.FeatureType;
 
 /**
  * Helper methods for the metadata created by {@code DataStore} implementations.
- * This is not yet a general-purpose builder suitable for public API, since the
+ * This is not a general-purpose builder suitable for public API, since the
  * methods provided in this class are tailored for Apache SIS data store needs.
  * API of this class may change in any future SIS versions.
  *
@@ -104,54 +123,241 @@ import org.opengis.feature.FeatureType;
  */
 public class MetadataBuilder {
     /**
-     * The metadata created by this reader, or {@code null} if none.
+     * Whether the next party to create should be an instance of {@link DefaultIndividual} or {@link DefaultOrganisation}.
+     *
+     * @see #party()
+     * @see #newParty(PartyType)
+     */
+    private PartyType partyType = PartyType.UNKNOWN;
+
+    /**
+     * Whether the next grid should be an instance of {@link DefaultGeorectified} or {@link DefaultGeoreferenceable}.
+     *
+     * @see #gridRepresentation()
+     * @see #newGridRepresentation(GridType)
+     */
+    private GridType gridType = GridType.UNSPECIFIED;
+
+    /**
+     * {@code true} if the next {@code CoverageDescription} to create will be a description of measurements
+     * in the electromagnetic spectrum. In that case, the coverage description will actually be an instance
+     * of {@code ImageDescription}.
+     *
+     * @see #coverageDescription()
+     * @see #newCoverage(boolean)
+     */
+    private boolean electromagnetic;
+
+    /**
+     * For using the same instance of {@code Integer} or {@code Double} when the value is the same.
+     * Also used for reusing {@link Citation} instances already created for a given title.
+     * Keys and values can be:
+     *
+     * <table class="sis">
+     *   <tr><th>Key</th>                          <th>Value</th>               <th>Method</th></tr>
+     *   <tr><td>{@link Integer}</td>              <td>{@link Integer}</td>     <td>{@link #shared(Integer)}</td></tr>
+     *   <tr><td>{@link Double}</td>               <td>{@link Double}</td>      <td>{@link #shared(Double)}</td></tr>
+     *   <tr><td>{@link Identifier}</td>           <td>{@link Identifier}</td>  <td>{@link #sharedIdentifier(CharSequence, String)}</td></tr>
+     *   <tr><td>{@link InternationalString}</td>  <td>{@link Citation}</td>    <td>{@link #sharedCitation(InternationalString)}</td></tr>
+     * </table>
+     */
+    private final Map<Object,Object> sharedValues = new HashMap<>();
+
+    // Other fields declared below together with closely related methods.
+
+    /**
+     * Creates a new metadata builder.
+     */
+    public MetadataBuilder() {
+    }
+
+    /**
+     * The metadata created by this builder, or {@code null} if not yet created.
      */
     private DefaultMetadata metadata;
 
     /**
+     * Creates the metadata object if it does not already exists, then returns it.
+     *
+     * @return the metadata (never {@code null}).
+     * @see #build(boolean)
+     */
+    private DefaultMetadata metadata() {
+        if (metadata == null) {
+            metadata = new DefaultMetadata();
+        }
+        return metadata;
+    }
+
+    /**
      * The identification information that are part of {@linkplain #metadata}, or {@code null} if none.
      */
     private DefaultDataIdentification identification;
 
     /**
+     * Creates the identification information object if it does not already exists, then returns it.
+     *
+     * @return the identification information (never {@code null}).
+     * @see #newIdentification()
+     */
+    private DefaultDataIdentification identification() {
+        if (identification == null) {
+            identification = new DefaultDataIdentification();
+        }
+        return identification;
+    }
+
+    /**
      * The citation of data {@linkplain #identification}, or {@code null} if none.
      */
     private DefaultCitation citation;
 
     /**
+     * Creates the citation object if it does not already exists, then returns it.
+     *
+     * @return the citation information (never {@code null}).
+     */
+    private DefaultCitation citation() {
+        if (citation == null) {
+            citation = new DefaultCitation();
+        }
+        return citation;
+    }
+
+    /**
      * Part of the responsible party of the {@linkplain #citation}, or {@code null} if none.
      */
     private DefaultResponsibility responsibility;
 
     /**
+     * Creates the responsibility object if it does not already exists, then returns it.
+     *
+     * @return the responsibility party (never {@code null}).
+     */
+    private DefaultResponsibility responsibility() {
+        if (responsibility == null) {
+            responsibility = new DefaultResponsibility();
+        }
+        return responsibility;
+    }
+
+    /**
      * Part of the responsible party of the {@linkplain #citation}, or {@code null} if none.
      */
     private AbstractParty party;
 
     /**
+     * Creates the individual or organization information object if it does not already exists, then returns it.
+     *
+     * <p><b>Limitation:</b> if the party type is unknown, then this method creates an {@code AbstractParty} instead
+     * than one of the subtypes. This is not valid, but we currently have no way to guess if a party is an individual
+     * or an organization. For now we prefer to let users know that the type is unknown rather than to pick a
+     * potentially wrong type.</p>
+     *
+     * @return the individual or organization information (never {@code null}).
+     * @see #newParty(MetadataBuilder.PartyType)
+     */
+    private AbstractParty party() {
+        if (party == null) {
+            switch (partyType) {
+                case UNKNOWN:      party = new AbstractParty();       break;
+                case INDIVIDUAL:   party = new DefaultIndividual();   break;
+                case ORGANISATION: party = new DefaultOrganisation(); break;
+                default:           throw new AssertionError(partyType);
+            }
+        }
+        return party;
+    }
+
+    /**
      * Copyright information, or {@code null} if none.
      */
     private DefaultLegalConstraints constraints;
 
     /**
+     * Creates the constraints information object if it does not already exists, then returns it.
+     *
+     * @return the constraints information (never {@code null}).
+     */
+    private DefaultLegalConstraints constraints() {
+        if (constraints == null) {
+            constraints = new DefaultLegalConstraints();
+        }
+        return constraints;
+    }
+
+    /**
      * The extent information that are part of {@linkplain #identification}, or {@code null} if none.
      */
     private DefaultExtent extent;
 
     /**
+     * Creates the extent information object if it does not already exists, then returns it.
+     *
+     * @return the extent information (never {@code null}).
+     */
+    private DefaultExtent extent() {
+        if (extent == null) {
+            extent = new DefaultExtent();
+        }
+        return extent;
+    }
+
+    /**
      * Information about the platforms and sensors that collected the data, or {@code null} if none.
      */
     private DefaultAcquisitionInformation acquisition;
 
     /**
+     * Creates the acquisition information object if it does not already exists, then returns it.
+     *
+     * @return the acquisition information (never {@code null}).
+     * @see #newAcquisition()
+     */
+    private DefaultAcquisitionInformation acquisition() {
+        if (acquisition == null) {
+            acquisition = new DefaultAcquisitionInformation();
+        }
+        return acquisition;
+    }
+
+    /**
      * Platform where are installed the sensors that collected the data, or {@code null} if none.
      */
     private DefaultPlatform platform;
 
     /**
-     * Information about the grid shape, or {@code null} if none.
+     * Creates a platform object if it does not already exists, then returns it.
+     *
+     * @return the platform information (never {@code null}).
+     */
+    private DefaultPlatform platform() {
+        if (platform == null) {
+            platform = new DefaultPlatform();
+        }
+        return platform;
+    }
+
+    /**
+     * Information about the feature types, or {@code null} if none.
+     */
+    private DefaultFeatureCatalogueDescription featureDescription;
+
+    /**
+     * Creates the feature descriptions object if it does not already exists, then returns it.
+     * This method sets the {@code includedWithDataset} property to {@code true} because the
+     * metadata built by this helper class are typically encoded together with the data.
+     *
+     * @return the feature descriptions (never {@code null}).
+     * @see #newFeatureTypes()
      */
-    private DefaultGeorectified gridRepresentation;
+    private DefaultFeatureCatalogueDescription featureDescription() {
+        if (featureDescription == null) {
+            featureDescription = new DefaultFeatureCatalogueDescription();
+            featureDescription.setIncludedWithDataset(true);
+        }
+        return featureDescription;
+    }
 
     /**
      * Information about the content of a grid data cell, or {@code null} if none.
@@ -160,9 +366,17 @@ public class MetadataBuilder {
     private DefaultCoverageDescription coverageDescription;
 
     /**
-     * Information about the feature types, or {@code null} if none.
+     * Creates the coverage description object if it does not already exists, then returns it.
+     *
+     * @return the coverage description (never {@code null}).
+     * @see #newCoverage(boolean)
      */
-    private DefaultFeatureCatalogueDescription featureDescription;
+    private DefaultCoverageDescription coverageDescription() {
+        if (coverageDescription == null) {
+            coverageDescription = electromagnetic ? new DefaultImageDescription() : new DefaultCoverageDescription();
+        }
+        return coverageDescription;
+    }
 
     /**
      * Information about content type for groups of attributes for a specific range dimension, or {@code null} if none.
@@ -170,86 +384,172 @@ public class MetadataBuilder {
     private DefaultAttributeGroup attributeGroup;
 
     /**
-     * The characteristic of each dimension (layer) included in the resource, or {@code null} if none.
-     */
-    private DefaultSampleDimension sampleDimension;
-
-    /**
-     * The distribution format, or {@code null} if none.
-     * This is part of the resource {@linkplain #identification}.
+     * Creates the attribute group object if it does not already exists, then returns it.
+     *
+     * @return the attribute group (never {@code null}).
      */
-    private Format format;
+    private DefaultAttributeGroup attributeGroup() {
+        if (attributeGroup == null) {
+            attributeGroup = new DefaultAttributeGroup();
+        }
+        return attributeGroup;
+    }
 
     /**
-     * Information about the events or source data used in constructing the data specified by the
-     * {@linkplain DefaultLineage#getScope() scope}.
+     * The characteristic of each dimension (layer) included in the resource, or {@code null} if none.
      */
-    private DefaultLineage lineage;
+    private DefaultSampleDimension sampleDimension;
 
     /**
-     * Information about an event or transformation in the life of a resource.
-     * This is part of {@link #lineage}.
+     * Creates the sample dimension object if it does not already exists, then returns it.
+     *
+     * @return the sample dimension (never {@code null}).
+     * @see #newSampleDimension()
      */
-    private DefaultProcessStep processStep;
+    private DefaultSampleDimension sampleDimension() {
+        if (sampleDimension == null) {
+            sampleDimension = new DefaultSampleDimension();
+        }
+        return sampleDimension;
+    }
 
     /**
-     * Information about the procedures, processes and algorithms applied in the process step.
-     * This is part of {@link #processStep}.
+     * Information about the grid shape, or {@code null} if none.
      */
-    private DefaultProcessing processing;
+    private DefaultGridSpatialRepresentation gridRepresentation;
 
     /**
-     * Whether the next party to create should be an instance of {@link DefaultIndividual} or {@link DefaultOrganisation}.
+     * Creates a grid representation object if it does not already exists, then returns it.
      *
-     * @see #newParty(PartyType)
+     * @return the grid representation object (never {@code null}).
+     * @see #newGridRepresentation(GridType)
      */
-    private PartyType partyType = PartyType.UNKNOWN;
+    private DefaultGridSpatialRepresentation gridRepresentation() {
+        if (gridRepresentation == null) {
+            switch (gridType) {
+                case GEORECTIFIED:     gridRepresentation = new DefaultGeorectified(); break;
+                case GEOREFERENCEABLE: gridRepresentation = new DefaultGeoreferenceable(); break;
+                default:               gridRepresentation = new DefaultGridSpatialRepresentation(); break;
+            }
+        }
+        return gridRepresentation;
+    }
 
     /**
-     * {@code true} if the next {@code CoverageDescription} to create will be a description of measurements
-     * in the electromagnetic spectrum. In that case, the coverage description will actually be an instance
-     * of {@code ImageDescription}.
+     * Information about the distributor of and options for obtaining the resource.
      */
-    private boolean electromagnetic;
+    private DefaultDistribution distribution;
 
     /**
-     * For using the same instance of {@code Double} when the value is the same.
-     * We use this map because the same values appear many time in a Landsat file.
+     * Creates the distribution information object if it does not already exists, then returns it.
      *
-     * @see #shared(Integer)
-     * @see #shared(Double)
+     * @return the distribution information (never {@code null}).
+     * @see #newDistribution()
      */
-    private final Map<Number,Number> sharedNumbers = new HashMap<>();
+    private DefaultDistribution distribution() {
+        if (distribution == null) {
+            distribution = new DefaultDistribution();
+        }
+        return distribution;
+    }
 
     /**
-     * Creates a new metadata reader.
+     * The distribution format, or {@code null} if none.
+     * This is part of the resource {@linkplain #identification}.
      */
-    public MetadataBuilder() {
-    }
+    private Format format;
 
     /**
-     * Adds the given element in the given collection if not already present. This method is used only for
-     * properties that are usually stored in {@code List} rather than {@code Set} and for which we do not
-     * keep a reference in this {@code MetadataBuilder} after the element has been added. This method is
-     * intended for adding elements that despite being modifiable, are not going to be modified by this
-     * {@code MetadataBuilder} class. Performance should not be a concern since the given list is usually
-     * very short (0 or 1 element).
+     * Creates the distribution format object if it does not already exists, then returns it.
      *
-     * <p>The given element should be non-null. The check for null value should be done by the caller instead
-     * than by this method in order to avoid unneeded creation of collections. Such creation are implicitly
-     * done by calls to {@code metadata.getFoos()} methods.</p>
+     * @return the distribution format (never {@code null}).
      */
-    private static <E> void addIfNotPresent(final Collection<E> collection, final E element) {
-        if (!collection.contains(element)) {
-            collection.add(element);
+    private DefaultFormat format() {
+        DefaultFormat df = DefaultFormat.castOrCopy(format);
+        if (df == null) {
+            format = df = new DefaultFormat();
         }
+        return df;
     }
 
     /**
-     * The type of party to create (individual, organization or unknown).
+     * Information about the events or source data used in constructing the data specified by the
+     * {@linkplain DefaultLineage#getScope() scope}.
      */
-    public enum PartyType {
-        /**
+    private DefaultLineage lineage;
+
+    /**
+     * Creates the lineage object if it does not already exists, then returns it.
+     *
+     * @return the lineage (never {@code null}).
+     * @see #newLineage()
+     */
+    private DefaultLineage lineage() {
+        if (lineage == null) {
+            lineage = new DefaultLineage();
+        }
+        return lineage;
+    }
+
+    /**
+     * Information about an event or transformation in the life of a resource.
+     * This is part of {@link #lineage}.
+     */
+    private DefaultProcessStep processStep;
+
+    /**
+     * Creates the process step object if it does not already exists, then returns it.
+     *
+     * @return the process step (never {@code null}).
+     */
+    private DefaultProcessStep processStep() {
+        if (processStep == null) {
+            processStep = new DefaultProcessStep();
+        }
+        return processStep;
+    }
+
+    /**
+     * Information about the procedures, processes and algorithms applied in the process step.
+     * This is part of {@link #processStep}.
+     */
+    private DefaultProcessing processing;
+
+    /**
+     * Creates the processing object if it does not already exists, then returns it.
+     *
+     * @return the processing (never {@code null}).
+     */
+    private DefaultProcessing processing() {
+        if (processing == null) {
+            processing = new DefaultProcessing();
+        }
+        return processing;
+    }
+
+    /**
+     * Adds the given element in the given collection if not already present. This method is used only for
+     * properties that are usually stored in {@code List} rather than {@code Set} and for which we do not
+     * keep a reference in this {@code MetadataBuilder} after the element has been added. This method is
+     * intended for adding elements that despite being modifiable, are not going to be modified by this
+     * {@code MetadataBuilder} class. Performance should not be a concern since the given list is usually
+     * very short (0 or 1 element).
+     *
+     * <p>The given element should be non-null. The check for null value should be done by the caller instead
+     * than by this method in order to avoid unneeded creation of collections. Such creation are implicitly
+     * done by calls to {@code metadata.getFoos()} methods.</p>
+     */
+    private static <E> void addIfNotPresent(final Collection<E> collection, final E element) {
+        if (!collection.contains(element)) {
+            collection.add(element);
+        }
+    }
+
+    /**
+     * The type of party to create (individual, organization or unknown).
+     */
+    public enum PartyType {
+        /**
          * Instructs {@link #newParty(PartyType)} that the next party to create should be an instance of
          * {@link DefaultIndividual}.
          */
@@ -345,21 +645,31 @@ public class MetadataBuilder {
     }
 
     /**
-     * Commits all pending information under the metadata "spatial representation" node (dimensions, <i>etc</i>).
-     * If there is no pending spatial representation information, then invoking this method has no effect.
-     * If new spatial representation info are added after this method call, they will be stored in a new element.
+     * Commits all pending information under metadata "distribution" node.
+     * If there is no pending distribution information, then invoking this method has no effect.
+     * If new distribution info are added after this method call, they will be stored in a new element.
      *
-     * <p>This method does not need to be invoked unless a new "spatial representation info" node,
+     * <p>This method does not need to be invoked unless a new "distribution info" node,
      * separated from the previous one, is desired.</p>
      */
-    public final void newGridRepresentation() {
-        if (gridRepresentation != null) {
-            final int n = gridRepresentation.getAxisDimensionProperties().size();
-            if (n != 0) {
-                gridRepresentation.setNumberOfDimensions(shared(n));
-            }
-            addIfNotPresent(metadata.getSpatialRepresentationInfo(), gridRepresentation);
-            gridRepresentation = null;
+    public final void newDistribution() {
+        if (distribution != null) {
+            addIfNotPresent(metadata().getDistributionInfo(), distribution);
+            distribution = null;
+        }
+    }
+
+    /**
+     * Commits all pending information under the metadata "feature catalog" node.
+     * If there is no pending feature description, then invoking this method has no effect.
+     * If new feature descriptions are added after this method call, they will be stored in a new element.
+     *
+     * <p>This method does not need to be invoked unless a new "feature catalog description" node is desired.</p>
+     */
+    public final void newFeatureTypes() {
+        if (featureDescription != null) {
+            addIfNotPresent(metadata().getContentInfo(), featureDescription);
+            featureDescription = null;
         }
     }
 
@@ -376,10 +686,7 @@ public class MetadataBuilder {
      *         will be a description of measurements in the electromagnetic spectrum.
      */
     public final void newCoverage(final boolean electromagnetic) {
-        if (sampleDimension != null) {
-            addIfNotPresent(attributGroup().getAttributes(), sampleDimension);
-            sampleDimension = null;
-        }
+        newSampleDimension();
         if (attributeGroup != null) {
             addIfNotPresent(coverageDescription().getAttributeGroups(), attributeGroup);
             attributeGroup = null;
@@ -392,17 +699,61 @@ public class MetadataBuilder {
     }
 
     /**
-     * Commits all pending information under the metadata "feature catalog" node.
-     * If there is no pending feature description, then invoking this method has no effect.
-     * If new feature descriptions are added after this method call, they will be stored in a new element.
+     * Commits all pending information under the coverage "attribute group" node.
+     * If there is no pending sample dimension description, then invoking this method has no effect.
+     * If new sample dimensions are added after this method call, they will be stored in a new element.
      *
-     * <p>This method does not need to be invoked unless a new "feature catalog description" node is desired.</p>
+     * <p>This method does not need to be invoked unless a new "sample dimension" node is desired.</p>
      */
-    public final void newFeatureTypes() {
-        if (featureDescription != null) {
-            addIfNotPresent(metadata().getContentInfo(), featureDescription);
-            featureDescription = null;
+    public final void newSampleDimension() {
+        if (sampleDimension != null) {
+            addIfNotPresent(attributeGroup().getAttributes(), sampleDimension);
+            sampleDimension = null;
+        }
+    }
+
+    /**
+     * The type of grid spatial representation (georectified, georeferenceable or unspecified).
+     */
+    public enum GridType {
+        /**
+         * Grid is an instance of {@link org.opengis.metadata.spatial.Georectified}.
+         */
+        GEORECTIFIED,
+
+        /**
+         * Grid is an instance of {@link org.opengis.metadata.spatial.Georeferenceable}.
+         */
+        GEOREFERENCEABLE,
+
+        /**
+         * Grid is neither georectified or georeferenceable.
+         * A plain {@link org.opengis.metadata.spatial.GridSpatialRepresentation} instance will be used.
+         */
+        UNSPECIFIED
+    }
+
+    /**
+     * Commits all pending information under the metadata "spatial representation" node (dimensions, <i>etc</i>).
+     * If there is no pending spatial representation information, then invoking this method has no effect.
+     * If new spatial representation info are added after this method call, they will be stored in a new element.
+     *
+     * <p>This method does not need to be invoked unless a new "spatial representation info" node,
+     * separated from the previous one, is desired.</p>
+     *
+     * @param type  whether the next grid should be an instance of {@link DefaultGeorectified} or {@link DefaultGeoreferenceable}.
+     */
+    public final void newGridRepresentation(final GridType type) {
+        ArgumentChecks.ensureNonNull("type", type);
+        if (gridRepresentation != null) {
+            final int n = gridRepresentation.getAxisDimensionProperties().size();
+            if (n != 0) {
+                gridRepresentation.setNumberOfDimensions(shared(n));
+            }
+            addIfNotPresent(metadata.getSpatialRepresentationInfo(), gridRepresentation);
+            gridRepresentation = null;
         }
+        gridType = type;
     }
 
     /**
@@ -429,541 +780,517 @@ public class MetadataBuilder {
     }
 
     /**
-     * Creates the metadata object if it does not already exists, then returns it.
+     * Creates or fetches a citation for the given title. The same citation may be shared by many metadata objects,
+     * for example identifiers or groups of keywords. Current implementation creates a {@link DefaultCitation} for
+     * the given title and caches the result. Future implementations may return predefined citation constants from
+     * the SQL database when applicable.
      *
-     * @return the metadata (never {@code null}).
+     * @param  title  the citation title, or {@code null} if none.
+     * @return a (potentially shared) citation for the given title, or {@code null} if the given title was null.
      */
-    private DefaultMetadata metadata() {
-        if (metadata == null) {
-            metadata = new DefaultMetadata();
+    private Citation sharedCitation(final InternationalString title) {
+        if (title == null) return null;
+        Citation c = (Citation) sharedValues.get(title);
+        if (c == null) {
+            c = new DefaultCitation(title);
+            sharedValues.put(title, c);
         }
-        return metadata;
+        return c;
     }
 
     /**
-     * Creates the identification information object if it does not already exists, then returns it.
+     * Creates or fetches an identifier for the given authority and code. This method may query the metadata
+     * database for fetching a more complete {@link Citation} for the given {@code authority}.
+     * This method may return a shared {@code Identifier} instance.
      *
-     * @return the identification information (never {@code null}).
+     * @param  authority  the authority tile, or {@code null} if none.
+     * @param  code       the identifier code (mandatory).
      */
-    private DefaultDataIdentification identification() {
-        if (identification == null) {
-            identification = new DefaultDataIdentification();
-        }
-        return identification;
+    private Identifier sharedIdentifier(final CharSequence authority, final String code) {
+        final DefaultIdentifier id = new DefaultIdentifier(sharedCitation(trim(authority)), code);
+        return (Identifier) JDK8.getOrDefault(sharedValues, id, id);
     }
 
     /**
-     * Creates the citation object if it does not already exists, then returns it.
-     *
-     * @return the citation information (never {@code null}).
+     * Specify if an information apply to data, to metadata or to both.
+     * This is used for setting the locale or character encoding.
      */
-    private DefaultCitation citation() {
-        if (citation == null) {
-            citation = new DefaultCitation();
-        }
-        return citation;
+    public enum Scope {
+        /**
+         * Information applies only to the resource (data).
+         */
+        RESOURCE,
+
+        /**
+         * Information applies only to metadata.
+         */
+        METADATA,
+
+        /**
+         * Information applies to both resource and metadata.
+         */
+        ALL
     }
 
     /**
-     * Creates the responsibility object if it does not already exists, then returns it.
+     * Adds a resource (data) identifier, a metadata identifier, or both as they are often the same.
+     * The identifier is added only if {@code code} is non-null, regardless other argument values.
+     * Empty strings (ignoring spaces) are ignored.
+     * Storages locations are:
      *
-     * @return the responsibility party (never {@code null}).
+     * <ul>
+     *   <li><b>Metadata:</b> {@code metadata/metadataIdentifier}</li>
+     *   <li><b>Resource:</b> {@code metadata/identificationInfo/citation/identifier}</li>
+     * </ul>
+     *
+     * @param  authority  the the person or party responsible for maintenance of the namespace, or {@code null} if none.
+     * @param  code       the identifier code, or {@code null} for no-operation.
+     * @param  scope      whether the date applies to data, to metadata or to both.
+     *
+     * @see #addTitle(CharSequence)
      */
-    private DefaultResponsibility responsibility() {
-        if (responsibility == null) {
-            responsibility = new DefaultResponsibility();
+    public final void addIdentifier(final CharSequence authority, String code, final Scope scope) {
+        ArgumentChecks.ensureNonNull("scope", scope);
+        if (code != null && !(code = code.trim()).isEmpty()) {
+            final Identifier id = sharedIdentifier(authority, code);
+            if (scope != Scope.RESOURCE) metadata().setMetadataIdentifier(id);
+            if (scope != Scope.METADATA) addIfNotPresent(citation().getIdentifiers(), id);
         }
-        return responsibility;
     }
 
     /**
-     * Creates the individual or organization information object if it does not already exists, then returns it.
+     * Sets the file format. The given name should be a short name like "GeoTIFF".
+     * The long name will be inferred from the given short name, if possible.
+     * Storage location is:
      *
-     * <p><b>Limitation:</b> if the party type is unknown, then this method creates an {@code AbstractParty} instead
-     * than one of the subtypes. This is not valid, but we currently have no way to guess if a party is an individual
-     * or an organization. For now we prefer to let users know that the type is unknown rather than to pick a
-     * potentially wrong type.</p>
+     * <ul>
+     *   <li>{@code metadata/identificationInfo/resourceFormat/formatSpecificationCitation/alternateTitle}</li>
+     * </ul>
      *
-     * @return the individual or organization information (never {@code null}).
+     * @param  abbreviation  the format short name or abbreviation, or {@code null} for no-operation.
+     * @throws MetadataStoreException  if this method can not connect to the {@code jdbc/SpatialMetadata} database.
+     *         Callers should generally handle this exception as a recoverable one (i.e. log a warning and continue).
+     *
+     * @see #addCompression(CharSequence)
      */
-    private AbstractParty party() {
-        if (party == null) {
-            switch (partyType) {
-                case UNKNOWN:      party = new AbstractParty();       break;
-                case INDIVIDUAL:   party = new DefaultIndividual();   break;
-                case ORGANISATION: party = new DefaultOrganisation(); break;
-                default:           throw new AssertionError(partyType);
+    public final void setFormat(final String abbreviation) throws MetadataStoreException {
+        if (abbreviation != null && abbreviation.length() != 0) {
+            if (format == null) {
+                format = MetadataSource.getProvided().lookup(Format.class, abbreviation);
             }
         }
-        return party;
     }
 
     /**
-     * Creates the constraints information object if it does not already exists, then returns it.
+     * Adds a language used for documenting data and/or metadata.
+     * Storage locations are:
      *
-     * @return the constraints information (never {@code null}).
+     * <ul>
+     *   <li><b>Metadata:</b> {@code metadata/language}</li>
+     *   <li><b>Resource:</b> {@code metadata/identificationInfo/language}</li>
+     * </ul>
+     *
+     * @param  language  a language used for documenting data and/or metadata, or {@code null} for no-operation.
+     * @param  scope     whether the language applies to data, to metadata or to both.
+     *
+     * @see #addEncoding(Charset, MetadataBuilder.Scope)
      */
-    private DefaultLegalConstraints constraints() {
-        if (constraints == null) {
-            constraints = new DefaultLegalConstraints();
+    public final void addLanguage(final Locale language, final Scope scope) {
+        ArgumentChecks.ensureNonNull("scope", scope);
+        if (language != null) {
+            // No need to use 'addIfNotPresent(…)' because Locale collection is a Set by default.
+            if (scope != Scope.RESOURCE) metadata().getLanguages().add(language);
+            if (scope != Scope.METADATA) identification().getLanguages().add(language);
         }
-        return constraints;
     }
 
     /**
-     * Creates the extent information object if it does not already exists, then returns it.
+     * Adds a character set used for encoding the data and/or metadata.
+     * Storage locations are:
      *
-     * @return the extent information (never {@code null}).
+     * <ul>
+     *   <li><b>Metadata:</b> {@code metadata/characterSet}</li>
+     *   <li><b>Resource:</b> {@code metadata/identificationInfo/characterSet}</li>
+     * </ul>
+     *
+     * @param  encoding  the character set used for encoding data and/or metadata, or {@code null} for no-operation.
+     * @param  scope     whether the encoding applies to data, to metadata or to both.
+     *
+     * @see #addLanguage(Locale, MetadataBuilder.Scope)
      */
-    private DefaultExtent extent() {
-        if (extent == null) {
-            extent = new DefaultExtent();
+    public final void addEncoding(final Charset encoding, final Scope scope) {
+        ArgumentChecks.ensureNonNull("scope", scope);
+        if (encoding != null) {
+            // No need to use 'addIfNotPresent(…)' because Charset collection is a Set by default.
+            if (scope != Scope.RESOURCE) metadata().getCharacterSets().add(encoding);
+            if (scope != Scope.METADATA) identification().getCharacterSets().add(encoding);
         }
-        return extent;
     }
 
     /**
-     * Creates the acquisition information object if it does not already exists, then returns it.
+     * Adds information about the scope of the resource.
+     * The scope is typically {@link ScopeCode#DATASET}.
+     * Storage locations are:
      *
-     * @return the acquisition information (never {@code null}).
-     */
-    private DefaultAcquisitionInformation acquisition() {
-        if (acquisition == null) {
-            acquisition = new DefaultAcquisitionInformation();
-        }
-        return acquisition;
-    }
-
-    /**
-     * Creates a platform object if it does not already exists, then returns it.
+     * <ul>
+     *   <li>{@code metadata/metadataScope/resourceScope}</li>
+     *   <li>{@code metadata/metadataScope/name}</li>
+     * </ul>
      *
-     * @return the platform information (never {@code null}).
+     * @param  scope  the scope of the resource, or {@code null} if none.
+     * @param  name   description of the scope, or {@code null} if none.
      */
-    private DefaultPlatform platform() {
-        if (platform == null) {
-            platform = new DefaultPlatform();
+    public final void addResourceScope(final ScopeCode scope, final CharSequence name) {
+        if (scope != null || name != null) {
+            addIfNotPresent(metadata().getMetadataScopes(), new DefaultMetadataScope(scope, name));
         }
-        return platform;
     }
 
     /**
-     * Creates a grid representation object if it does not already exists, then returns it.
+     * Adds a date of the given type. This is not the data acquisition time, but rather the metadata creation
+     * or last update time. With {@link Scope#METADATA}, this is the creation of the metadata file as a whole.
+     * With {@link Scope#RESOURCE}, this is the creation of the metadata for a particular "identification info".
+     * They are often the same, since there is typically only one "identification info" per file.
+     * Storage locations are:
      *
-     * @return the grid representation object (never {@code null}).
+     * <ul>
+     *   <li><b>Metadata:</b> {@code metadata/dateInfo/*}</li>
+     *   <li><b>Resource:</b> {@code metadata/identificationInfo/citation/date/*}</li>
+     * </ul>
+     *
+     * If a date already exists for the given type, then the earliest date is retained (oldest date are discarded)
+     * except for {@link DateType#LAST_REVISION}, {@link DateType#LAST_UPDATE LAST_UPDATE} or any other date type
+     * prefixed by {@code "LATE_"}, where only the latest date is kept.
+     *
+     * @param  date   the date to add, or {@code null} for no-operation..
+     * @param  type   the type of the date to add, or {@code null} if none (not legal but tolerated).
+     * @param  scope  whether the date applies to data, to metadata or to both.
+     *
+     * @see #addAcquisitionTime(Date)
      */
-    private DefaultGeorectified gridRepresentation() {
-        if (gridRepresentation == null) {
-            gridRepresentation = new DefaultGeorectified();
+    public final void addCitationDate(final Date date, final DateType type, final Scope scope) {
+        ArgumentChecks.ensureNonNull("scope", scope);
+        if (date != null) {
+            final DefaultCitationDate cd = new DefaultCitationDate(date, type);
+            if (scope != Scope.RESOURCE) addEarliest(metadata().getDateInfo(), cd, type);
+            if (scope != Scope.METADATA) addEarliest(citation().getDates(),    cd, type);
         }
-        return gridRepresentation;
     }
 
     /**
-     * Creates the sample dimension object if it does not already exists, then returns it.
-     *
-     * @return the sample dimension (never {@code null}).
-     */
-    private DefaultSampleDimension sampleDimension() {
-        if (sampleDimension == null) {
-            sampleDimension = new DefaultSampleDimension();
+     * Adds a date in the given collection, making sure that there is no two dates of the same type.
+     * If two dates are of the same type, retains the latest one if the type name starts with {@code "LATE_"}
+     * or retains the earliest date otherwise.
+     */
+    private static void addEarliest(final Collection<CitationDate> dates, final CitationDate cd, final DateType type) {
+        for (final Iterator<CitationDate> it = dates.iterator(); it.hasNext();) {
+            final CitationDate co = it.next();
+            if (type.equals(co.getDateType())) {
+                final Date oldDate = co.getDate();
+                final Date newDate = cd.getDate();
+                if (type.name().startsWith("LATE_") ? oldDate.before(newDate) : oldDate.after(newDate)) {
+                    it.remove();
+                    break;
+                }
+                return;
+            }
         }
-        return sampleDimension;
+        dates.add(cd);
     }
 
     /**
-     * Creates the attribut group object if it does not already exists, then returns it.
-     *
-     * @return the attribut group (never {@code null}).
+     * Returns the given character sequence as a non-empty character string with leading and trailing spaces removed.
+     * If the given character sequence is null, empty or blank, then this method returns {@code null}.
      */
-    private DefaultAttributeGroup attributGroup() {
-        if (attributeGroup == null) {
-            attributeGroup = new DefaultAttributeGroup();
+    private static InternationalString trim(CharSequence value) {
+        value = CharSequences.trimWhitespaces(value);
+        if (value != null && value.length() != 0) {
+            return Types.toInternationalString(value);
+        } else {
+            return null;
         }
-        return attributeGroup;
     }
 
     /**
-     * Creates the coverage description object if it does not already exists, then returns it.
-     *
-     * @return the coverage description (never {@code null}).
+     * Returns the concatenation of the given strings. The previous string may be {@code null}.
+     * This method does nothing if the previous string already contains the one to append.
      */
-    private DefaultCoverageDescription coverageDescription() {
-        if (coverageDescription == null) {
-            coverageDescription = electromagnetic ? new DefaultImageDescription() : new DefaultCoverageDescription();
+    private static InternationalString append(final InternationalString previous, final InternationalString toAdd) {
+        if (previous == null) {
+            return toAdd;
         }
-        return coverageDescription;
+        final String p = previous.toString();
+        final String a = toAdd.toString();
+        if (p.contains(a)) {
+            return previous;
+        }
+        return Types.toInternationalString(p + System.lineSeparator() + a);
     }
 
     /**
-     * Creates the feature descriptions object if it does not already exists, then returns it.
-     * This method sets the {@code includedWithDataset} property to {@code true} because the
-     * metadata built by this helper class are typically encoded together with the data.
-     *
-     * @return the feature descriptions (never {@code null}).
+     * Returns {@code true} if the given character sequences have equal content.
      */
-    private DefaultFeatureCatalogueDescription featureDescription() {
-        if (featureDescription == null) {
-            featureDescription = new DefaultFeatureCatalogueDescription();
-            featureDescription.setIncludedWithDataset(true);
+    private static boolean equals(final CharSequence s1, final CharSequence s2) {
+        if (s1 == s2) {
+            return true;
         }
-        return featureDescription;
+        if (s1 == null || s2 == null) {
+            return false;
+        }
+        return s1.toString().equals(s2.toString());
     }
 
     /**
-     * Creates the distribution format object if it does not already exists, then returns it.
+     * Adds a title or alternate title of the resource.
+     * Storage location is:
      *
-     * @return the distribution format (never {@code null}).
+     * <ul>
+     *   <li>{@code metadata/identificationInfo/citation/title} if available</li>
+     *   <li>{@code metadata/identificationInfo/citation/alternateTitle} otherwise</li>
+     * </ul>
+     *
+     * @param title  the resource title or alternate title, or {@code null} for no-operation.
+     *
+     * @see #addAbstract(CharSequence)
      */
-    private DefaultFormat format() {
-        DefaultFormat df = DefaultFormat.castOrCopy(format);
-        if (df == null) {
-            format = df = new DefaultFormat();
+    public final void addTitle(final CharSequence title) {
+        final InternationalString i18n = trim(title);
+        if (i18n != null) {
+            final DefaultCitation citation = citation();
+            final InternationalString current = citation.getTitle();
+            if (current == null) {
+                citation.setTitle(i18n);
+            } else if (!equals(current, i18n)) {
+                addIfNotPresent(citation.getAlternateTitles(), i18n);
+            }
         }
-        return df;
     }
 
     /**
-     * Creates the lineage object if it does not already exists, then returns it.
+     * Adds a brief narrative summary of the resource(s).
+     * If a summary already existed, the new one will be appended after a new line.
+     * Storage location is:
      *
-     * @return the lineage (never {@code null}).
+     * <ul>
+     *   <li>{@code metadata/identificationInfo/abstract}
+     * </ul>
+     *
+     * @param description  the summary of resource(s), or {@code null} for no-operation.
+     *
+     * @see #addTitle(CharSequence)
+     * @see #addPurpose(CharSequence)
      */
-    private DefaultLineage lineage() {
-        if (lineage == null) {
-            lineage = new DefaultLineage();
+    public final void addAbstract(final CharSequence description) {
+        final InternationalString i18n = trim(description);
+        if (i18n != null) {
+            final DefaultDataIdentification identification = identification();
+            identification.setAbstract(append(identification.getAbstract(), i18n));
         }
-        return lineage;
     }
 
     /**
-     * Creates the process step object if it does not already exists, then returns it.
+     * Adds a summary of the intentions with which the resource(s) was developed.
+     * If a purpose already existed, the new one will be appended after a new line.
+     * Storage location is:
      *
-     * @return the process step (never {@code null}).
+     * <ul>
+     *   <li>{@code metadata/identificationInfo/purpose}</li>
+     * </ul>
+     *
+     * @param intention  the summary of intention(s), or {@code null} for no-operation.
+     *
+     * @see #addAbstract(CharSequence)
      */
-    private DefaultProcessStep processStep() {
-        if (processStep == null) {
-            processStep = new DefaultProcessStep();
+    public final void addPurpose(final CharSequence intention) {
+        final InternationalString i18n = trim(intention);
+        if (i18n != null) {
+            final DefaultDataIdentification identification = identification();
+            identification.setPurpose(append(identification.getPurpose(), i18n));
         }
-        return processStep;
     }
 
     /**
-     * Creates the processing object if it does not already exists, then returns it.
+     * Adds other information required to complete the citation that is not recorded elsewhere.
+     * Storage location is:
      *
-     * @return the processing (never {@code null}).
+     * <ul>
+     *   <li>{@code metadata/identificationInfo/citation/otherCitationDetails}</li>
+     * </ul>
+     *
+     * @param  details  other details, or {@code null} for no-operation.
      */
-    private DefaultProcessing processing() {
-        if (processing == null) {
-            processing = new DefaultProcessing();
+    public final void addOtherCitationDetails(final CharSequence details) {
+        final InternationalString i18n = trim(details);
+        if (i18n != null) {
+            addIfNotPresent(citation().getOtherCitationDetails(), i18n);
         }
-        return processing;
     }
 
     /**
-     * Specify if an information apply to data, to metadata or to both.
+     * Adds any other descriptive information about the resource.
+     * If information already existed, the new one will be appended after a new line.
+     * Storage location is:
+     *
+     * <ul>
+     *   <li>{@code metadata/identificationInfo/supplementalInformation}</li>
+     * </ul>
+     *
+     * @param info  any other descriptive information about the resource, or {@code null} for no-operation.
      */
-    public enum Scope {
-        /**
-         * Information applies only to data.
-         */
-        DATA,
-
-        /**
-         * Information applies only to metadata.
-         */
-        METADATA,
-
-        /**
-         * Information applies to both data and metadata.
-         */
-        ALL
+    public final void addSupplementalInformation(final CharSequence info) {
+        final InternationalString i18n = trim(info);
+        if (i18n != null) {
+            final DefaultDataIdentification identification = identification();
+            identification.setSupplementalInformation(append(identification.getSupplementalInformation(), i18n));
+        }
     }
 
     /**
-     * Adds a language used for documenting data and/or metadata.
-     * Storage locations are:
+     * Adds a main theme of the resource.
+     * Storage location is:
      *
-     * <table class="compact" summary="Storage locations.">
-     * <tr><td>Metadata</td> <td>{@code metadata/language}</td></tr>
-     * <tr><td>Data</td>     <td>{@code metadata/identificationInfo/language}</td></tr>
-     * </table>
+     * <ul>
+     *   <li>{@code metadata/identificationInfo/topicCategory}</li>
+     * </ul>
      *
-     * @param  language  a language used for documenting data and/or metadata.
-     * @param  scope     whether the language applies to data, to metadata or to both.
+     * @param  topic  main theme of the resource, or {@code null} for no-operation.
      */
-    public final void add(final Locale language, final Scope scope) {
-        ArgumentChecks.ensureNonNull("scope", scope);
-        if (language != null) {
-            // No need to use 'addIfNotPresent(…)' because Locale collection is a Set by default.
-            if (scope != Scope.DATA)           metadata().getLanguages().add(language);
-            if (scope != Scope.METADATA) identification().getLanguages().add(language);
+    public final void addTopicCategory(final TopicCategory topic) {
+        if (topic != null) {
+            // No need to use 'addIfNotPresent(…)' for enumerations.
+            identification().getTopicCategories().add(topic);
         }
     }
 
     /**
-     * Adds a character set used for encoding the data and/or metadata.
+     * Adds keywords if at least one non-empty element exists in the {@code keywords} array.
+     * Other arguments have no impact on whether keywords are added or not because only the
+     * {@code MD_Keywords.keyword} property is mandatory.
      * Storage locations are:
      *
-     * <table class="compact" summary="Storage locations.">
-     * <tr><td>Metadata</td> <td>{@code metadata/characterSet}</td></tr>
-     * <tr><td>Data</td>     <td>{@code metadata/identificationInfo/characterSet}</td></tr>
-     * </table>
+     * <ul>
+     *   <li>{@code metadata/identificationInfo/descriptiveKeywords}</li>
+     *   <li>{@code metadata/identificationInfo/thesaurusName/title}</li>
+     *   <li>{@code metadata/identificationInfo/type}</li>
+     * </ul>
      *
-     * @param  encoding  the character set used for encoding data and/or metadata.
-     * @param  scope     whether the encoding applies to data, to metadata or to both.
-     */
-    public final void add(final Charset encoding, final Scope scope) {
-        ArgumentChecks.ensureNonNull("scope", scope);
-        if (encoding != null) {
-            // No need to use 'addIfNotPresent(…)' because Charset collection is a Set by default.
-            if (scope != Scope.DATA)           metadata().getCharacterSets().add(encoding);
-            if (scope != Scope.METADATA) identification().getCharacterSets().add(encoding);
+     * @param  keywords       word(s) used to describe the subject, or {@code null} for no-operation.
+     * @param  type           subject matter used to group similar keywords, or {@code null} if none.
+     * @param  thesaurusName  name of the formally registered thesaurus, or {@code null} if none.
+     */
+    public final void addKeywords(final Iterable<? extends CharSequence> keywords, final KeywordType type,
+            final CharSequence thesaurusName)
+    {
+        if (keywords != null) {
+            DefaultKeywords group = null;
+            Collection<InternationalString> list = null;
+            for (final CharSequence kw : keywords) {
+                final InternationalString i18n = trim(kw);
+                if (i18n != null) {
+                    if (list == null) {
+                        group = new DefaultKeywords();
+                        group.setType(type);
+                        group.setThesaurusName(sharedCitation(trim(thesaurusName)));
+                        list = group.getKeywords();
+                    }
+                    list.add(i18n);
+                }
+            }
+            if (group != null) {
+                addIfNotPresent(identification().getDescriptiveKeywords(), group);
+            }
         }
     }
 
     /**
-     * Adds information about the scope of the resource.
-     * The scope is typically {@link ScopeCode#DATASET}.
+     * Adds an author name. If an author was already defined with a different name,
+     * then a new party instance is created.
      * Storage location is:
      *
-     * <pre>metadata/metadataScope/resourceScope</pre>
+     * <ul>
+     *   <li>{@code metadata/identificationInfo/citation/party/name}</li>
+     * </ul>
      *
-     * @param  scope  the scope of the resource, or {@code null} if none.
+     * @param  name  the name of the author or publisher, or {@code null} for no-operation.
      */
-    public final void add(final ScopeCode scope) {
-        if (scope != null) {
-            addIfNotPresent(metadata().getMetadataScopes(), new DefaultMetadataScope(scope, null));
+    public final void addAuthor(final CharSequence name) {
+        final InternationalString i18n = trim(name);
+        if (i18n != null) {
+            if (party != null) {
+                final InternationalString current = party.getName();
+                if (current != null) {
+                    if (equals(current, name)) {
+                        return;
+                    }
+                    newParty(partyType);
+                }
+            }
+            party().setName(i18n);
         }
     }
 
     /**
-     * Adds descriptions for the given feature.
-     * Storage location is:
+     * Adds s role, name, contact and position information for an individual or organization that is responsible
+     * for the resource. This method can be used as an alternative to {@link #addAuthor(CharSequence)} when the
+     * caller needs to create the responsibly party itself.
      *
-     * <pre>metadata/contentInfo/featureTypes/featureTypeName</pre>
+     * <p>If the given {@code role} is non-null, then this method will ensure that the added party has the given
+     * role. A copy of the given party will be created if needed (the given party will never be modified).</p>
      *
-     * This method returns the feature name for more convenient chaining with
-     * {@link org.apache.sis.storage.FeatureNaming#add FeatureNaming.add(…)}.
-     * Note that the {@link FeatureCatalogBuilder} subclasses can also be used for that chaining.
+     * Storage locations are:
      *
-     * @param  type         the feature type to add, or {@code null}.
-     * @param  occurrences  number of instances of the given feature type, or {@code null} if unknown.
-     * @return the name of the added feature, or {@code null} if none.
+     * <ul>
+     *   <li>{@code metadata/identificationInfo/citation/citedResponsibleParty}</li>
+     *   <li>{@code metadata/identificationInfo/citation/citedResponsibleParty/role}</li>
+     * </ul>
      *
-     * @see FeatureCatalogBuilder#define(FeatureType)
+     * @param  party  the individual or organization that is responsible, or {@code null} for no-operation.
+     * @param  role   the role to set, or {@code null} for leaving it unchanged.
      */
-    public final GenericName add(final FeatureType type, final Integer occurrences) {
-        if (type != null) {
-            final GenericName name = type.getName();
-            if (name != null) {
-                final DefaultFeatureTypeInfo info = new DefaultFeatureTypeInfo(name);
-                if (occurrences != null) {
-                    info.setFeatureInstanceCount(shared(occurrences));
-                }
-                addIfNotPresent(featureDescription().getFeatureTypeInfo(), info);
-                return name;
+    public final void addCitedResponsibleParty(Responsibility party, final Role role) {
+        if (party != null) {
+            if (role != null && !role.equals(party.getRole())) {
+                party = new DefaultResponsibility(party);
+                ((DefaultResponsibility) party).setRole(role);
             }
+            addIfNotPresent(citation().getCitedResponsibleParties(), party);
         }
-        return null;
     }
 
     /**
-     * Adds the given coordinate reference system to metadata, if it does not already exists.
-     * This method ensures that there is no duplicated values.
-     * Storage location is:
+     * Adds a means of communication with person(s) and organizations(s) associated with the resource(s).
+     * This is often the same party than the above cited responsibly party, with only the role changed.
+     * Storage locations are:
      *
-     * <pre>metadata/referenceSystemInfo</pre>
+     * <ul>
+     *   <li><b>Metadata</b> {@code metadata/contact}</li>
+     *   <li><b>Resource</b> {@code metadata/identificationInfo/pointOfContact}</li>
+     * </ul>
      *
-     * @param  crs  the coordinate reference system to add to the metadata, or {@code null} if none.
+     * @param  contact  means of communication with party associated with the resource, or {@code null} for no-operation.
+     * @param  scope    whether the contact applies to data, to metadata or to both.
      */
-    public final void add(final CoordinateReferenceSystem crs) {
-        if (crs != null) {
-            addIfNotPresent(metadata().getReferenceSystemInfo(), crs);
+    public final void addPointOfContact(final Responsibility contact, final Scope scope) {
+        ArgumentChecks.ensureNonNull("scope", scope);
+        if (contact != null) {
+            if (scope != Scope.RESOURCE)     addIfNotPresent(metadata().getContacts(), contact);
+            if (scope != Scope.METADATA) addIfNotPresent(identification().getPointOfContacts(), contact);
         }
     }
 
     /**
-     * Adds the given envelope, including its CRS, to the metadata. If the metadata already contains a geographic
-     * bounding box, then a new bounding box is added; this method does not compute the union of the two boxes.
+     * Adds a distributor. This is often the same than the above responsible party.
      * Storage location is:
      *
-     * <pre>metadata/identificationInfo/extent/geographicElement</pre>
+     * <ul>
+     *   <li>{@code metadata/distributionInfo/distributor/distributorContact}</li>
+     * </ul>
      *
-     * @param  envelope  the extent to add in the metadata, or {@code null} if none.
-     * @throws TransformException if an error occurred while converting the given envelope to extents.
+     * @param  distributor  the distributor, or {@code null} for no-operation.
      */
-    public final void addExtent(final AbstractEnvelope envelope) throws TransformException {
-        if (envelope != null) {
-            add(envelope.getCoordinateReferenceSystem());
-            if (!envelope.isAllNaN()) {
-                extent().addElements(envelope);
-            }
-        }
-    }
-
-    /**
-     * Adds a geographic bounding box initialized to the values in the given array.
-     * The array must contains at least 4 values starting at the given index in this exact order:
-     *
-     * <ul>
-     *   <li>{@code westBoundLongitude} (the minimal λ value), or {@code NaN}</li>
-     *   <li>{@code eastBoundLongitude} (the maximal λ value), or {@code NaN}</li>
-     *   <li>{@code southBoundLatitude} (the minimal φ value), or {@code NaN}</li>
-     *   <li>{@code northBoundLatitude} (the maximal φ value), or {@code NaN}</li>
-     * </ul>
-     *
-     * Storage location is:
-     *
-     * <pre>metadata/identificationInfo/extent/geographicElement</pre>
-     *
-     * @param  ordinates  the geographic coordinates.
-     * @param  index      index of the first value to use in the given array.
-     */
-    public final void addExtent(final double[] ordinates, int index) {
-        final DefaultGeographicBoundingBox bbox = new DefaultGeographicBoundingBox(
-                    ordinates[index], ordinates[++index], ordinates[++index], ordinates[++index]);
-        if (!bbox.isEmpty()) {
-            addIfNotPresent(extent().getGeographicElements(), bbox);
-        }
-    }
-
-    /**
-     * Adds a temporal extent covered by the data.
-     * Storage location is:
-     *
-     * <pre>metadata/identificationInfo/extent/temporalElement</pre>
-     *
-     * @param  startTime  when the data begins, or {@code null}.
-     * @param  endTime    when the data ends, or {@code null}.
-     * @throws UnsupportedOperationException if the temporal module is not on the classpath.
-     *
-     * @see #addAcquisitionTime(Date)
-     */
-    public final void addExtent(final Date startTime, final Date endTime) {
-        if (startTime != null || endTime != null) {
-            final DefaultTemporalExtent t = new DefaultTemporalExtent();
-            t.setBounds(startTime, endTime);
-            addIfNotPresent(extent().getTemporalElements(), t);
-        }
-    }
-
-    /**
-     * Adds a date of the given type. This is not the data acquisition time,
-     * but rather the metadata creation or last update time.
-     * Storage location is:
-     *
-     * <pre>metadata/identificationInfo/citation/date/date</pre>
-     *
-     * @param date  the date to add, or {@code null}.
-     * @param type  the type of the date to add, or {@code null}.
-     */
-    public final void add(final Date date, final DateType type) {
-        if (date != null) {
-            addIfNotPresent(citation().getDates(), new DefaultCitationDate(date, type));
-        }
-    }
-
-    /**
-     * Returns the given character sequence as a non-empty character string with leading and trailing spaces removed.
-     * If the given character sequence is null, empty or blank, then this method returns {@code null}.
-     */
-    private static InternationalString trim(CharSequence value) {
-        value = CharSequences.trimWhitespaces(value);
-        if (value != null && value.length() != 0) {
-            return Types.toInternationalString(value);
-        } else {
-            return null;
-        }
-    }
-
-    /**
-     * Returns the concatenation of the given string. The previous string may be {@code null}.
-     * This method does nothing if the previous string already contains the one to append.
-     */
-    private static InternationalString append(final InternationalString previous, final InternationalString toAdd) {
-        if (previous == null) {
-            return toAdd;
-        }
-        final String p = previous.toString();
-        final String a = toAdd.toString();
-        if (p.contains(a)) {
-            return previous;
-        }
-        return Types.toInternationalString(p + System.lineSeparator() + a);
-    }
-
-    /**
-     * Returns {@code true} if the given character sequences have equal content.
-     */
-    private static boolean equals(final CharSequence s1, final CharSequence s2) {
-        if (s1 == s2) {
-            return true;
-        }
-        if (s1 == null || s2 == null) {
-            return false;
-        }
-        return s1.toString().equals(s2.toString());
-    }
-
-    /**
-     * Adds a title or alternate title of the resource.
-     * Storage location is:
-     *
-     * <pre>metadata/identificationInfo/citation/title</pre>
-     *
-     * @param title  the resource title or alternate title, or {@code null} if none.
-     */
-    public final void addTitle(final CharSequence title) {
-        final InternationalString i18n = trim(title);
-        if (i18n != null) {
-            final DefaultCitation citation = citation();
-            final InternationalString current = citation.getTitle();
-            if (current == null) {
-                citation.setTitle(i18n);
-            } else if (!equals(current, i18n)) {
-                addIfNotPresent(citation.getAlternateTitles(), i18n);
-            }
-        }
-    }
-
-    /**
-     * Adds a brief narrative summary of the resource(s).
-     * If a summary already existed, the new one will be appended after a new line.
-     * Storage location is:
-     *
-     * <pre>metadata/identificationInfo/abstract</pre>
-     *
-     * @param description  the summary of resource(s), or {@code null} if none.
-     */
-    public final void addAbstract(final CharSequence description) {
-        final InternationalString i18n = trim(description);
-        if (i18n != null) {
-            final DefaultDataIdentification identification = identification();
-            identification.setAbstract(append(identification.getAbstract(), i18n));
-        }
-    }
-
-    /**
-     * Adds an author name. If an author was already defined with a different name,
-     * then a new party instance is created.
-     * Storage location is:
-     *
-     * <pre>metadata/identificationInfo/citation/party/name</pre>
-     *
-     * @param  name  the name of the author or publisher, or {@code null} if none.
-     */
-    public final void addAuthor(final CharSequence name) {
-        final InternationalString i18n = trim(name);
-        if (i18n != null) {
-            if (party != null) {
-                final InternationalString current = party.getName();
-                if (current != null) {
-                    if (equals(current, name)) {
-                        return;
-                    }
-                    newParty(partyType);
-                }
-            }
-            party().setName(i18n);
+    public final void addDistributor(final Responsibility distributor) {
+        if (distributor != null) {
+            addIfNotPresent(distribution().getDistributors(), new DefaultDistributor(distributor));
         }
     }
 
@@ -971,9 +1298,11 @@ public class MetadataBuilder {
      * Adds recognition of those who contributed to the resource(s).
      * Storage location is:
      *
-     * <pre>metadata/identificationInfo/credit</pre>
+     * <ul>
+     *   <li>{@code metadata/identificationInfo/credit}</li>
+     * </ul>
      *
-     * @param  credit  recognition of those who contributed to the resource(s).
+     * @param  credit  recognition of those who contributed to the resource, or {@code null} for no-operation.
      */
     public final void addCredits(final CharSequence credit) {
         final InternationalString i18n = trim(credit);
@@ -983,21 +1312,6 @@ public class MetadataBuilder {
     }
 
     /**
-     * Adds a data identifier (not necessarily the same as the metadata identifier).
-     * Empty strings (ignoring spaces) are ignored.
-     * Storage location is:
-     *
-     * <pre>metadata/identificationInfo/citation/identifier</pre>
-     *
-     * @param  code  the identifier code, or {@code null} if none.
-     */
-    public final void addIdentifier(String code) {
-        if (code != null && !(code = code.trim()).isEmpty()) {
-            addIfNotPresent(citation().getIdentifiers(), new DefaultIdentifier(code));
-        }
-    }
-
-    /**
      * Elements to omit in the legal notice to be parsed by {@link MetadataBuilder#parseLegalNotice(String)}.
      * Some of those elements are implied by the metadata were the legal notice will be stored.
      */
@@ -1206,9 +1520,11 @@ parse:      for (int i = 0; i < length;)
      *
      * Storage location is:
      *
-     * <pre>metadata/identificationInfo/resourceConstraint</pre>
+     * <ul>
+     *   <li>{@code metadata/identificationInfo/resourceConstraint}</li>
+     * </ul>
      *
-     * @param  notice  the legal notice, or {@code null} if none.
+     * @param  notice  the legal notice, or {@code null} for no-operation.
      */
     public final void parseLegalNotice(final String notice) {
         if (notice != null) {
@@ -1217,163 +1533,557 @@ parse:      for (int i = 0; i < length;)
     }
 
     /**
-     * Adds a platform on which instrument are installed. If a platform was already defined
-     * with a different identifier, then a new platform instance will be created.
+     * Adds an access constraint applied to assure the protection of privacy or intellectual property,
+     * and any special restrictions or limitations on obtaining the resource.
      * Storage location is:
      *
-     * <pre>metadata/acquisitionInformation/platform/identifier</pre>
+     * <ul>
+     *   <li>{@code metadata/identificationInfo/resourceConstraint/accessConstraints}</li>
+     * </ul>
      *
-     * @param  identifier  identifier of the platform to add, or {@code null}.
+     * @param  restriction  access constraints applied, or {@code null} for no-operation.
      */
-    public final void addPlatform(String identifier) {
-        if (identifier != null && !(identifier = identifier.trim()).isEmpty()) {
-            if (platform != null) {
-                final Identifier current = platform.getIdentifier();
-                if (current != null) {
-                    if (identifier.equals(current.getCode())) {
-                        return;
-                    }
-                    acquisition().getPlatforms().add(platform);
-                    platform = null;
+    public final void addAccessConstraint(final Restriction restriction) {
+        if (restriction != null) {
+            // No need to use 'addIfNotPresent(…)' for code lists.
+            constraints().getAccessConstraints().add(restriction);
+        }
+    }
+
+    /**
+     * Adds a limitation affecting the fitness for use of the resource.
+     * Storage location is:
+     *
+     * <ul>
+     *   <li>{@code metadata/identificationInfo/resourceConstraint/useLimitation}</li>
+     * </ul>
+     *
+     * @param  limitation  limitation affecting the fitness for use, or {@code null} for no-operation.
+     */
+    public final void addUseLimitation(final CharSequence limitation) {
+        final InternationalString i18n = trim(limitation);
+        if (i18n != null) {
+            addIfNotPresent(constraints().getUseLimitations(), i18n);
+        }
+    }
+
+    /**
+     * Adds the given coordinate reference system to metadata, if it does not already exists.
+     * This method ensures that there is no duplicated values.
+     * Storage location is:
+     *
+     * <ul>
+     *   <li>{@code metadata/referenceSystemInfo}</li>
+     * </ul>
+     *
+     * @param  crs  the coordinate reference system to add to the metadata, or {@code null} for no-operation.
+     */
+    public final void addReferenceSystem(final CoordinateReferenceSystem crs) {
+        if (crs != null) {
+            addIfNotPresent(metadata().getReferenceSystemInfo(), crs);
+        }
+    }
+
+    /**
+     * Adds a geographic extent described by an identifier. The given identifier is stored as-is as
+     * the natural language description, and possibly in a modified form as the geographic identifier.
+     * See {@link DefaultGeographicDescription#DefaultGeographicDescription(CharSequence)} for details.
+     * Storage locations are:
+     *
+     * <ul>
+     *   <li>{@code metadata/identificationInfo/extent/geographicElement/description}</li>
+     *   <li>{@code metadata/identificationInfo/extent/geographicElement/identifier}</li>
+     * </ul>
+     *
+     * @param  identifier  identifier or description of spatial and temporal extent, or {@code null} for no-operation.
+     */
+    public final void addExtent(final CharSequence identifier) {
+        final InternationalString i18n = trim(identifier);
+        if (i18n != null) {
+            addIfNotPresent(extent().getGeographicElements(), new DefaultGeographicDescription(identifier));
+        }
+    }
+
+    /**
+     * Adds the given envelope, including its CRS, to the metadata. If the metadata already contains a geographic
+     * bounding box, then a new bounding box is added; this method does not compute the union of the two boxes.
+     * Storage location is:
+     *
+     * <ul>
+     *   <li>{@code metadata/identificationInfo/extent/geographicElement}</li>
+     * </ul>
+     *
+     * @param  envelope  the extent to add in the metadata, or {@code null} for no-operation.
+     * @throws TransformException if an error occurred while converting the given envelope to extents.
+     */
+    public final void addExtent(final AbstractEnvelope envelope) throws TransformException {
+        if (envelope != null) {
+            addReferenceSystem(envelope.getCoordinateReferenceSystem());
+            if (!envelope.isAllNaN()) {
+                extent().addElements(envelope);
+            }
+        }
+    }
+
+    /**
+     * Adds a geographic bounding box initialized to the values in the given array.
+     * The array must contains at least 4 values starting at the given index in this exact order:
+     *
+     * <ul>
+     *   <li>{@code westBoundLongitude} (the minimal λ value), or {@code NaN}</li>
+     *   <li>{@code eastBoundLongitude} (the maximal λ value), or {@code NaN}</li>
+     *   <li>{@code southBoundLatitude} (the minimal φ value), or {@code NaN}</li>
+     *   <li>{@code northBoundLatitude} (the maximal φ value), or {@code NaN}</li>
+     * </ul>
+     *
+     * Storage location is:
+     *
+     * <ul>
+     *   <li>{@code metadata/identificationInfo/extent/geographicElement}</li>
+     * </ul>
+     *
+     * @param  ordinates  the geographic coordinates, or {@code null} for no-operation.
+     * @param  index      index of the first value to use in the given array.
+     */
+    public final void addExtent(final double[] ordinates, int index) {
+        if (ordinates != null) {
+            final DefaultGeographicBoundingBox bbox = new DefaultGeographicBoundingBox(
+                        ordinates[index], ordinates[++index], ordinates[++index], ordinates[++index]);
+            if (!bbox.isEmpty()) {
+                addIfNotPresent(extent().getGeographicElements(), bbox);
+            }
+        }
+    }
+
+    /**
+     * Adds a vertical extent covered by the data.
+     * Storage location is:
+     *
+     * <ul>
+     *   <li>{@code metadata/identificationInfo/extent/verticalElement}</li>
+     * </ul>
+     *
+     * @param  minimumValue  the lowest vertical extent contained in the dataset, or {@link Double#NaN} if none.
+     * @param  maximumValue  the highest vertical extent contained in the dataset, or {@link Double#NaN} if none.
+     * @param  verticalCRS   the information about the vertical coordinate reference system, or {@code null}.
+     */
+    public final void addVerticalExtent(final double minimumValue,
+                                        final double maximumValue,
+                                        final VerticalCRS verticalCRS)
+    {
+        if (!Double.isNaN(minimumValue) || !Double.isNaN(maximumValue) || verticalCRS != null) {
+            addIfNotPresent(extent().getVerticalElements(),
+                            new DefaultVerticalExtent(minimumValue, maximumValue, verticalCRS));
+        }
+    }
+
+    /**
+     * Adds a temporal extent covered by the data.
+     * Storage location is:
+     *
+     * <ul>
+     *   <li>{@code metadata/identificationInfo/extent/temporalElement}</li>
+     * </ul>
+     *
+     * @param  startTime  when the data begins, or {@code null} if unbounded.
+     * @param  endTime    when the data ends, or {@code null} if unbounded.
+     * @throws UnsupportedOperationException if the temporal module is not on the classpath.
+     *
+     * @see #addAcquisitionTime(Date)
+     */
+    public final void addTemporalExtent(final Date startTime, final Date endTime) {
+        if (startTime != null || endTime != null) {
+            final DefaultTemporalExtent t = new DefaultTemporalExtent();
+            t.setBounds(startTime, endTime);
+            addIfNotPresent(extent().getTemporalElements(), t);
+        }
+    }
+
+    /**
+     * Adds descriptions for the given feature.
+     * Storage location is:
+     *
+     * <ul>
+     *   <li>{@code metadata/contentInfo/featureTypes/featureTypeName}</li>
+     *   <li>{@code metadata/contentInfo/featureTypes/featureInstanceCount}</li>
+     * </ul>
+     *
+     * This method returns the feature name for more convenient chaining with
+     * {@link org.apache.sis.storage.FeatureNaming#add FeatureNaming.add(…)}.
+     * Note that the {@link FeatureCatalogBuilder} subclasses can also be used for that chaining.
+     *
+     * @param  type         the feature type to add, or {@code null} for no-operation.
+     * @param  occurrences  number of instances of the given feature type, or {@code null} if unknown.
+     * @return the name of the added feature, or {@code null} if none.
+     *
+     * @see FeatureCatalogBuilder#define(FeatureType)
+     */
+    public final GenericName addFeatureType(final FeatureType type, final Integer occurrences) {
+        if (type != null) {
+            final GenericName name = type.getName();
+            if (name != null) {
+                final DefaultFeatureTypeInfo info = new DefaultFeatureTypeInfo(name);
+                if (occurrences != null) {
+                    info.setFeatureInstanceCount(shared(occurrences));
                 }
+                addIfNotPresent(featureDescription().getFeatureTypeInfo(), info);
+                return name;
             }
-            platform().setIdentifier(new DefaultIdentifier(identifier));
         }
+        return null;
     }
 
     /**
-     * Adds an instrument or sensor on the platform.
+     * Adds a method used to spatially represent geographic information.
      * Storage location is:
      *
-     * <pre>metadata/acquisitionInformation/platform/instrument/identifier</pre>
+     * <ul>
+     *   <li>{@code metadata/identificationInfo/spatialRepresentationType}</li>
+     * </ul>
      *
-     * @param  identifier  identifier of the sensor to add, or {@code null}.
+     * @param  type  method used to spatially represent geographic information, or {@code null} for no-operation.
      */
-    public final void addInstrument(String identifier) {
-        if (identifier != null && !(identifier = identifier.trim()).isEmpty()) {
-            final DefaultInstrument instrument = new DefaultInstrument();
-            instrument.setIdentifier(new DefaultIdentifier(identifier));
-            addIfNotPresent(platform().getInstruments(), instrument);
+    public final void addSpatialRepresentation(final SpatialRepresentationType type) {
+        if (type != null) {
+            // No need to use 'addIfNotPresent(…)' for code lists.
+            identification().getSpatialRepresentationTypes().add(type);
+        }
+    }
+
+    /**
+     * Adds a linear resolution in metres.
+     * Storage location is:
+     *
+     * <ul>
+     *   <li>{@code metadata/identificationInfo/spatialResolution/distance}</li>
+     * </ul>
+     *
+     * @param  distance  the resolution in metres, or {@code NaN} for no-operation.
+     */
+    public final void addResolution(final double distance) {
+        if (!Double.isNaN(distance)) {
+            final DefaultResolution r = new DefaultResolution();
+            r.setDistance(shared(distance));
+            addIfNotPresent(identification().getSpatialResolutions(), r);
+        }
+    }
+
+    /**
+     * Sets identification of grid data as point or cell.
+     * Storage location is:
+     *
+     * <ul>
+     *   <li>{@code metadata/spatialRepresentationInfo/cellGeometry}</li>
+     * </ul>
+     *
+     * @param  value   whether the data represent point or area, or {@code null} for no-operation.
+     */
+    public final void setCellGeometry(final CellGeometry value) {
+        if (value != null) {
+            gridRepresentation().setCellGeometry(value);
+        }
+    }
+
+    /**
+     * Sets the point in a pixel corresponding to the Earth location of the pixel.
+     * Storage location is:
+     *
+     * <ul>
+     *   <li>{@code metadata/spatialRepresentationInfo/pointInPixel}</li>
+     * </ul>
+     *
+     * @param  value   whether the data represent point or area, or {@code null} for no-operation.
+     */
+    public final void setPointInPixel(final PixelOrientation value) {
+        if (value != null) {
+            ((DefaultGeorectified) gridRepresentation()).setPointInPixel(value);
+        }
+    }
+
+    /**
+     * Sets a general description of the transformation form grid coordinates to "real world" coordinates.
+     * Storage location is:
+     *
+     * <ul>
+     *   <li>{@code metadata/spatialRepresentationInfo/transformationDimensionDescription}</li>
+     * </ul>
+     *
+     * @param  value  a general description of the "grid to CRS" transformation, or {@code null} for no-operation.
+     */
+    public final void setGridToCRS(final CharSequence value) {
+        final InternationalString i18n = trim(value);
+        if (i18n != null) {
+            ((DefaultGeorectified) gridRepresentation()).setTransformationDimensionDescription(i18n);
+        }
+    }
+
+    /**
+     * Returns the axis at the given dimension index. All previous dimensions are created if needed.
+     *
+     * @param  index  index of the desired dimension.
+     * @return dimension at the given index.
+     */
+    private DefaultDimension axis(final int index) {
+        final List<Dimension> axes = gridRepresentation().getAxisDimensionProperties();
+        for (int i=axes.size(); i <= index; i++) {
+            axes.add(new DefaultDimension());
+        }
+        return (DefaultDimension) axes.get(index);
+    }
+
+    /**
+     * Sets the number of cells along the given dimension.
+     * Storage location is:
+     *
+     * <ul>
+     *   <li>{@code metadata/spatialRepresentationInfo/axisDimensionProperties/dimensionName}</li>
+     * </ul>
+     *
+     * @param  dimension  the axis dimension.
+     * @param  name       the name to set for the given dimension.
+     */
+    public final void setAxisName(final int dimension, final DimensionNameType name) {
+        axis(dimension).setDimensionName(name);
+    }
+
+    /**
+     * Sets the number of cells along the given dimension.
+     * Storage location is:
+     *
+     * <ul>
+     *   <li>{@code metadata/spatialRepresentationInfo/axisDimensionProperties/dimensionSize}</li>
+     * </ul>
+     *
+     * @param  dimension  the axis dimension.
+     * @param  length     number of cell values along the given dimension.
+     */
+    public final void setAxisLength(final int dimension, final int length) {
+        axis(dimension).setDimensionSize(shared(length));
+    }
+
+    /**
+     * Sets the degree of detail in the given dimension.
+     * Storage location is:
+     *
+     * <ul>
+     *   <li>{@code metadata/spatialRepresentationInfo/axisDimensionProperties/resolution}</li>
+     * </ul>
+     *
+     * @param  dimension   the axis dimension.
+     * @param  resolution  the degree of detail in the grid dataset, or NaN for no-operation.
+     */
+    public final void setAxisResolution(final int dimension, final double resolution) {
+        if (!Double.isNaN(resolution)) {
+            axis(dimension).setResolution(shared(resolution));
+        }
+    }
+
+    /**
+     * Sets the name or number that uniquely identifies instances of bands of wavelengths on which a sensor operates.
+     * If a coverage contains more than one band, additional bands can be created by calling
+     * {@link #newSampleDimension()} before to call this method.
+     * Storage location is:
+     *
+     * <ul>
+     *   <li>{@code metadata/contentInfo/attributeGroup/attribute/sequenceIdentifier}</li>
+     * </ul>
+     *
+     * @param  sequenceIdentifier  the band name or number, or {@code null} for no-operation.
+     */
+    public final void setBandIdentifier(final MemberName sequenceIdentifier) {
+        if (sequenceIdentifier != null) {
+            sampleDimension().setSequenceIdentifier(sequenceIdentifier);
+        }
+    }
+
+    /**
+     * Adds an identifier for the current band.
+     * These identifiers can be use to provide names for the attribute from a standard set of names.
+     * If a coverage contains more than one band, additional bands can be created by calling
+     * {@link #newSampleDimension()} before to call this method.
+     * Storage location is:
+     *
+     * <ul>
+     *   <li>{@code metadata/contentInfo/attributeGroup/attribute/name}</li>
+     * </ul>
+     *
+     * @param  authority  identifies which controlled list of name is used, or {@code null} if none.
+     * @param  name       the band name, or {@code null} for no-operation.
+     */
+    public final void addBandIdentifier(final CharSequence authority, String name) {
+        if (name != null && !(name = name.trim()).isEmpty()) {
+            addIfNotPresent(sampleDimension().getNames(), sharedIdentifier(authority, name));
+        }
+    }
+
+    /**
+     * Adds a description of the current band.
+     * If a coverage contains more than one band, additional bands can be created by calling
+     * {@link #newSampleDimension()} before to call this method.
+     * Storage location is:
+     *
+     * <ul>

[... 602 lines stripped ...]


Mime
View raw message