sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 01/01: Merge branch 'geoapi-3.1'
Date Fri, 28 Dec 2018 20:03:51 GMT
This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 15af6618a728e005c54fbb3ae92c50e9d4842a0b
Merge: 475ebea 2caa9ba
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Fri Dec 28 21:02:56 2018 +0100

    Merge branch 'geoapi-3.1'

 application/pom.xml                                |  12 +-
 .../java/org/apache/sis/console/AboutCommand.java  |   2 +-
 .../org/apache/sis/console/TransformCommand.java   |   2 +-
 .../apache/sis/test/suite/ConsoleTestSuite.java    |   2 +-
 application/sis-openoffice/pom.xml                 |  10 +-
 .../org/apache/sis/openoffice/Registration.java    | 130 +--
 .../org/apache/sis/openoffice/Transformer.java     |   2 +-
 .../sis-openoffice/src/main/unopkg/description.xml |   2 +-
 .../apache/sis/test/suite/OpenOfficeTestSuite.java |   2 +-
 .../sis/internal/unopkg/FilteredJarFile.java       | 120 ---
 .../org/apache/sis/internal/unopkg/JavaMaker.java  |  89 +-
 .../org/apache/sis/internal/unopkg/UnoPkg.java     | 198 ++---
 .../apache/sis/internal/unopkg/package-info.java   |   9 +-
 .../org/apache/sis/feature/AbstractAttribute.java  |   4 +-
 .../org/apache/sis/feature/CharacteristicMap.java  |   2 +-
 .../org/apache/sis/feature/DefaultFeatureType.java |   2 +-
 .../org/apache/sis/feature/EnvelopeOperation.java  |   2 +-
 .../java/org/apache/sis/feature/FeatureFormat.java |   2 +-
 .../main/java/org/apache/sis/feature/Features.java |   2 +-
 .../java/org/apache/sis/feature/SparseFeature.java |   2 +-
 .../java/org/apache/sis/feature/Validator.java     |   2 +-
 .../java/org/apache/sis/feature/benchmarks.html    |   2 +-
 .../sis/feature/builder/FeatureTypeBuilder.java    |   8 +-
 .../apache/sis/feature/builder/TypeBuilder.java    |   2 +-
 .../sis/internal/feature/AttributeConvention.java  |   2 +-
 .../java/org/apache/sis/internal/feature/ESRI.java |   2 +-
 .../apache/sis/feature/CharacteristicMapTest.java  |   2 +-
 .../apache/sis/feature/DefaultFeatureTypeTest.java |   2 +-
 .../apache/sis/test/suite/FeatureTestSuite.java    |   2 +-
 .../java/org/apache/sis/internal/jaxb/Context.java |   6 +-
 .../sis/internal/jaxb/SpecializedIdentifier.java   |   2 +-
 .../apache/sis/internal/jaxb/gco/GO_DateTime.java  |   2 +-
 .../apache/sis/internal/jaxb/gco/GO_Distance.java  |   2 +-
 .../apache/sis/internal/jaxb/gco/GO_Measure.java   |   2 +-
 .../apache/sis/internal/jaxb/gco/package-info.java |   2 +-
 .../apache/sis/internal/jaxb/gml/DateAdapter.java  |   2 +-
 .../apache/sis/internal/jaxb/gml/GMLAdapter.java   |   2 +-
 .../org/apache/sis/internal/jaxb/gml/Measure.java  |   7 +-
 .../internal/jaxb/gml/UniversalTimeAdapter.java    |   2 +-
 .../apache/sis/internal/jaxb/lan/PT_FreeText.java  |   3 +-
 .../sis/internal/metadata/AxisDirections.java      |  76 +-
 .../apache/sis/internal/metadata/AxisNames.java    |  20 +-
 .../internal/metadata/LegacyPropertyAdapter.java   |   2 +-
 .../org/apache/sis/internal/metadata/Merger.java   |   2 +-
 .../sis/internal/metadata/ReferencingServices.java |  20 +-
 .../apache/sis/internal/metadata/Resources.java    |   5 +
 .../sis/internal/metadata/Resources.properties     |   1 +
 .../sis/internal/metadata/Resources_fr.properties  |   1 +
 .../sis/internal/metadata/ServicesForUtility.java  |   2 +-
 .../sis/internal/metadata/VerticalDatumTypes.java  |   2 -
 .../sis/internal/metadata/sql/Initializer.java     |   2 +-
 .../org/apache/sis/internal/xml/XmlUtilities.java  |   2 +-
 .../org/apache/sis/io/wkt/FormattableObject.java   |   3 +-
 .../main/java/org/apache/sis/io/wkt/Formatter.java |  71 +-
 .../apache/sis/io/wkt/GeodeticObjectParser.java    |  10 +-
 .../org/apache/sis/io/wkt/MathTransformParser.java |   2 +-
 .../java/org/apache/sis/io/wkt/Transliterator.java |  16 +-
 .../main/java/org/apache/sis/io/wkt/WKTFormat.java |  51 +-
 .../main/java/org/apache/sis/io/wkt/Warnings.java  |   2 +-
 .../org/apache/sis/metadata/PropertyAccessor.java  |   8 +-
 .../apache/sis/metadata/ValueExistencePolicy.java  |   2 +-
 .../org/apache/sis/metadata/iso/ISOMetadata.java   |   8 +-
 .../sis/metadata/iso/ImmutableIdentifier.java      |   2 +-
 .../metadata/iso/acquisition/DefaultObjective.java |   2 +-
 .../sis/metadata/iso/acquisition/package-info.java |   2 +-
 .../sis/metadata/iso/citation/Citations.java       |   4 +-
 .../sis/metadata/iso/citation/DefaultCitation.java |   2 +-
 .../sis/metadata/iso/citation/package-info.java    |   2 +-
 .../sis/metadata/iso/constraint/package-info.java  |   2 +-
 .../sis/metadata/iso/content/package-info.java     |   2 +-
 .../metadata/iso/distribution/package-info.java    |   2 +-
 .../iso/extent/DefaultGeographicBoundingBox.java   |   9 +-
 .../metadata/iso/extent/DefaultTemporalExtent.java |   2 +-
 .../apache/sis/metadata/iso/extent/Extents.java    |   6 +-
 .../sis/metadata/iso/extent/package-info.java      |   2 +-
 .../metadata/iso/identification/package-info.java  |   2 +-
 .../sis/metadata/iso/lineage/package-info.java     |   2 +-
 .../sis/metadata/iso/maintenance/package-info.java |   2 +-
 .../org/apache/sis/metadata/iso/package-info.java  |   2 +-
 .../sis/metadata/iso/quality/package-info.java     |   2 +-
 .../sis/metadata/iso/spatial/package-info.java     |   2 +-
 .../apache/sis/metadata/sql/CachedStatement.java   |  15 +-
 .../apache/sis/metadata/sql/MetadataSource.java    |   6 +-
 .../apache/sis/util/iso/DefaultRecordSchema.java   |   2 +-
 .../org/apache/sis/util/iso/DefaultTypeName.java   |   4 +-
 .../org/apache/sis/util/iso/GlobalNameSpace.java   |   2 +-
 .../main/java/org/apache/sis/util/iso/Names.java   |  44 +
 .../java/org/apache/sis/xml/MarshalContext.java    |   2 +-
 .../java/org/apache/sis/xml/MarshallerPool.java    |   2 +-
 .../main/java/org/apache/sis/xml/NilReason.java    |   2 +-
 .../java/org/apache/sis/xml/ReferenceResolver.java |   2 +-
 .../src/main/java/org/apache/sis/xml/XLink.java    |  18 +-
 .../src/main/java/org/apache/sis/xml/XML.java      |   4 +-
 .../internal/jaxb/ModifiableIdentifierMapTest.java |   2 +-
 .../org/apache/sis/io/wkt/TransliteratorTest.java  |   8 +-
 .../sis/metadata/iso/extent/ExtentsTest.java       |   2 +-
 .../java/org/apache/sis/test/MetadataAssert.java   |  10 +-
 .../apache/sis/test/suite/MetadataTestSuite.java   |   2 +-
 .../sis/test/xml/AnnotationConsistencyCheck.java   |   2 +-
 .../apache/sis/test/xml/DocumentComparator.java    |   4 +-
 .../java/org/apache/sis/test/xml/TestCase.java     |   6 +-
 .../java/org/apache/sis/coverage/Category.java     | 488 +++++++++++
 .../java/org/apache/sis/coverage/CategoryList.java | 691 +++++++++++++++
 .../org/apache/sis/coverage/ConvertedCategory.java |  74 ++
 .../org/apache/sis/coverage/ConvertedRange.java    | 105 +++
 .../org/apache/sis/coverage/SampleDimension.java   | 967 +++++++++++++++++++++
 .../org/apache/sis/coverage/SampleRangeFormat.java | 321 +++++++
 .../main/java/org/apache/sis/coverage/ToNaN.java   | 102 +++
 .../org/apache/sis/coverage/grid/GridChange.java   | 476 ++++++++++
 .../org/apache/sis/coverage/grid/GridCoverage.java | 185 ++++
 .../org/apache/sis/coverage/grid/GridExtent.java   | 441 +++++++---
 .../org/apache/sis/coverage/grid/GridGeometry.java | 501 ++++++++---
 .../apache/sis/coverage/grid/GridRoundingMode.java |  64 ++
 .../apache/sis/coverage/grid/PixelTranslation.java |  19 +-
 .../org/apache/sis/coverage}/package-info.java     |  14 +-
 .../sis/internal/raster/ColorModelFactory.java     | 499 +++++++++++
 .../sis/internal/raster/ColorModelPatch.java       | 121 +++
 .../internal/raster/MultiBandsIndexColorModel.java | 236 +++++
 .../apache/sis/internal/raster/RasterFactory.java  | 140 +++
 .../org/apache/sis/internal/raster/Resources.java  |  45 +
 .../sis/internal/raster/Resources.properties       |   9 +
 .../sis/internal/raster/Resources_fr.properties    |   9 +
 .../sis/internal/raster/ScaledColorSpace.java      | 171 ++++
 .../org/apache/sis/coverage/CategoryListTest.java  | 344 ++++++++
 .../java/org/apache/sis/coverage/CategoryTest.java | 242 ++++++
 .../apache/sis/coverage/SampleDimensionTest.java   | 116 +++
 .../apache/sis/coverage/grid/GridChangeTest.java   | 104 +++
 .../apache/sis/coverage/grid/GridExtentTest.java   |  86 +-
 .../apache/sis/coverage/grid/GridGeometryTest.java | 146 +++-
 .../org/apache/sis/image/DefaultIteratorTest.java  |   5 +-
 .../java/org/apache/sis/image/ImageTestCase.java   | 172 ++++
 .../test/java/org/apache/sis/image/TestViewer.java | 237 +++++
 .../image/{TiledImage.java => TiledImageMock.java} |  26 +-
 .../sis/internal/raster/ScaledColorSpaceTest.java  | 104 +++
 .../org/apache/sis/test/suite/RasterTestSuite.java |   9 +-
 .../sis/referencing/gazetteer/LocationFormat.java  |   2 +-
 .../gazetteer/MilitaryGridReferenceSystem.java     |   6 +-
 .../gazetteer/ModifiableLocationType.java          |  12 +-
 .../referencing/gazetteer/LocationTypeTest.java    |   2 +-
 .../gazetteer/ReferencingByIdentifiersTest.java    |   2 +-
 .../suite/ReferencingByIdentifiersTestSuite.java   |   2 +-
 .../sis/geometry/AbstractDirectPosition.java       |   8 +-
 .../org/apache/sis/geometry/AbstractEnvelope.java  |   6 +-
 .../org/apache/sis/geometry/ArrayEnvelope.java     |   3 +-
 .../org/apache/sis/geometry/CoordinateFormat.java  |   4 +-
 .../org/apache/sis/geometry/DirectPosition2D.java  |   4 +-
 .../org/apache/sis/geometry/EnvelopeReducer.java   | 165 ++++
 .../java/org/apache/sis/geometry/Envelopes.java    | 121 ++-
 .../apache/sis/geometry/GeneralDirectPosition.java |   4 +-
 .../org/apache/sis/geometry/GeneralEnvelope.java   |   2 +
 .../java/org/apache/sis/geometry/Shapes2D.java     |   6 +-
 .../referencing/CC_GeneralOperationParameter.java  |   2 +-
 .../referencing/CC_OperationParameterGroup.java    |   4 +-
 .../internal/referencing/CoordinateOperations.java |   5 +-
 .../internal/referencing/DefinitionVerifier.java   |   4 +-
 .../internal/referencing/DirectPositionView.java   |  10 +
 .../referencing/GeodeticObjectBuilder.java         |  86 +-
 .../apache/sis/internal/referencing/LazySet.java   |   2 +-
 .../referencing/PositionalAccuracyConstant.java    |   2 +-
 .../referencing/ReferencingFactoryContainer.java   | 215 +++++
 .../apache/sis/internal/referencing/Resources.java |  11 +
 .../sis/internal/referencing/Resources.properties  |   2 +
 .../internal/referencing/Resources_fr.properties   |   2 +
 .../internal/referencing/ServicesForMetadata.java  |  66 +-
 .../sis/internal/referencing/provider/Affine.java  |   2 +-
 .../referencing/provider/GeographicOffsets.java    |  12 +-
 .../provider/GeographicToGeocentric.java           |   2 +-
 .../internal/referencing/provider/Molodensky.java  |   4 +-
 .../referencing/provider/VerticalOffset.java       |   7 +-
 .../sis/parameter/DefaultParameterValue.java       |   4 +-
 .../sis/parameter/MapProjectionParameters.java     |   8 +-
 .../org/apache/sis/parameter/ParameterBuilder.java |   2 +-
 .../org/apache/sis/parameter/ParameterFormat.java  |   7 +-
 .../org/apache/sis/parameter/Parameterized.java    |   2 +-
 .../org/apache/sis/parameter/TensorValues.java     |   2 +-
 .../sis/referencing/AbstractIdentifiedObject.java  |   2 +-
 .../main/java/org/apache/sis/referencing/CRS.java  |  14 +-
 .../java/org/apache/sis/referencing/CommonCRS.java |  41 +-
 .../sis/referencing/PropertiesConverter.java       |   2 +-
 .../sis/referencing/StandardDefinitions.java       |   4 +-
 .../sis/referencing/crs/AbstractDerivedCRS.java    |   2 +-
 .../sis/referencing/crs/DefaultCompoundCRS.java    |   4 +-
 .../sis/referencing/crs/DefaultProjectedCRS.java   |   2 +-
 .../sis/referencing/crs/DefaultTemporalCRS.java    |  96 +-
 .../org/apache/sis/referencing/cs/AbstractCS.java  |  66 +-
 .../apache/sis/referencing/cs/AxesConvention.java  | 104 ++-
 .../java/org/apache/sis/referencing/cs/Codes.java  |  58 +-
 .../sis/referencing/cs/CoordinateSystems.java      | 161 +++-
 .../cs/DefaultCoordinateSystemAxis.java            |   2 +-
 .../org/apache/sis/referencing/cs/Normalizer.java  |  98 ++-
 .../sis/referencing/datum/DefaultEllipsoid.java    |   2 +-
 .../referencing/datum/DefaultGeodeticDatum.java    |   4 +-
 .../factory/CommonAuthorityFactory.java            |   2 +-
 .../factory/ConcurrentAuthorityFactory.java        |   2 +-
 .../factory/GeodeticAuthorityFactory.java          |   4 +-
 .../referencing/factory/sql/AuthorityCodes.java    |   2 +-
 .../referencing/factory/sql/EPSGDataAccess.java    |  12 +-
 .../sis/referencing/factory/sql/EPSGFactory.java   |   6 +-
 .../operation/AbstractSingleOperation.java         |   6 +-
 .../operation/CoordinateOperationFinder.java       |   4 +-
 .../operation/CoordinateOperationRegistry.java     |  10 +-
 .../operation/DefaultConcatenatedOperation.java    |   2 +-
 .../referencing/operation/DefaultConversion.java   |   6 +-
 .../operation/DefaultOperationMethod.java          |   2 +-
 .../operation/InverseOperationMethod.java          |   2 +-
 .../operation/builder/LinearTransformBuilder.java  |   3 +-
 .../operation/matrix/GeneralMatrix.java            |  16 +-
 .../referencing/operation/matrix/MatrixSIS.java    |  53 +-
 .../sis/referencing/operation/matrix/Solver.java   |   5 +-
 .../referencing/operation/projection/Mercator.java |   2 +-
 .../operation/projection/NormalizedProjection.java |   2 +-
 .../transform/AbstractLinearTransform.java         |  21 +-
 .../operation/transform/AbstractMathTransform.java |  49 +-
 .../operation/transform/ConcatenatedTransform.java | 218 ++---
 .../operation/transform/ConstantTransform1D.java   |   4 +
 .../transform/DefaultMathTransformFactory.java     |   4 +-
 .../transform/InterpolatedGeocentricTransform.java |   2 +-
 .../operation/transform/LinearInterpolator1D.java  |   2 +-
 .../operation/transform/MathTransformProvider.java |   2 +-
 .../operation/transform/MathTransforms.java        | 130 ++-
 .../transform/MathTransformsOrFactory.java         | 129 +++
 .../operation/transform/PassThroughTransform.java  | 473 +++++++---
 .../operation/transform/ProjectiveTransform.java   |  25 +-
 .../operation/transform/ScaleTransform.java        |  23 +-
 .../transform/SpecializableTransform.java          |   2 +-
 .../transform/SpecializableTransform1D.txt         |   6 +-
 .../operation/transform/SphericalToCartesian.java  |   2 +-
 .../operation/transform/TransferFunction.java      |  16 +-
 .../operation/transform/TransformSeparator.java    | 201 ++++-
 ...aleTransform.java => TranslationTransform.java} | 136 ++-
 .../operation/transform/package-info.java          |   2 +-
 .../apache/sis/geometry/EnvelopeReducerTest.java   |  95 ++
 .../org/apache/sis/geometry/EnvelopesTest.java     |  22 +
 .../sis/internal/metadata/AxisDirectionsTest.java  |  46 +-
 .../referencing/ServicesForMetadataTest.java       |  31 +-
 .../internal/referencing/provider/NADCONTest.java  |   2 +-
 .../internal/referencing/provider/NTv2Test.java    |   2 +-
 .../sis/io/wkt/GeodeticObjectParserTest.java       |   4 +-
 .../java/org/apache/sis/io/wkt/WKTFormatTest.java  |   2 +-
 .../sis/parameter/MapProjectionParametersTest.java |   2 +-
 .../org/apache/sis/referencing/CommonCRSTest.java  |  21 +-
 .../sis/referencing/crs/AbstractCRSTest.java       |   4 +-
 .../referencing/crs/DefaultEngineeringCRSTest.java |  14 +-
 .../referencing/crs/DefaultGeographicCRSTest.java  |  12 +-
 .../apache/sis/referencing/cs/AbstractCSTest.java  |   8 +-
 .../org/apache/sis/referencing/cs/CodesTest.java   |  10 +-
 .../sis/referencing/cs/CoordinateSystemsTest.java  |  38 +-
 .../sis/referencing/cs/DefaultCartesianCSTest.java |   4 +-
 .../cs/DefaultCoordinateSystemAxisTest.java        |   6 +-
 .../referencing/cs/DefaultCylindricalCSTest.java   |   2 +-
 .../referencing/cs/DefaultEllipsoidalCSTest.java   |   4 +-
 .../sis/referencing/cs/DefaultPolarCSTest.java     |   2 +-
 .../sis/referencing/cs/DefaultSphericalCSTest.java |   6 +-
 .../apache/sis/referencing/cs/HardCodedAxes.java   |  40 +-
 .../org/apache/sis/referencing/cs/HardCodedCS.java |   2 +-
 .../apache/sis/referencing/cs/NormalizerTest.java  | 176 +++-
 .../builder/LinearTransformBuilderTest.java        |   2 +-
 .../operation/matrix/GeneralMatrixTest.java        |  17 +-
 .../referencing/operation/matrix/Matrix2Test.java  |   2 +-
 .../referencing/operation/matrix/Matrix3Test.java  |  13 +-
 .../referencing/operation/matrix/Matrix4Test.java  |  42 +-
 .../operation/matrix/MatrixTestCase.java           |  60 +-
 .../operation/matrix/NonSquareMatrixTest.java      |  17 +-
 .../operation/projection/Benchmark.java            |   2 +-
 .../operation/projection/InitializerTest.java      |   2 +-
 .../projection/MercatorMethodComparison.java       |   5 +-
 .../projection/ObliqueStereographicTest.java       |   2 +-
 .../transform/AbridgedMolodenskyTransformTest.java |   4 +-
 .../transform/ConcatenatedTransformTest.java       |  10 +-
 .../operation/transform/MathTransformTestCase.java |  13 +-
 .../operation/transform/MathTransformsTest.java    |  96 +-
 .../transform/PassThroughTransformTest.java        | 141 ++-
 .../operation/transform/PseudoTransform.java       |  11 +-
 .../transform/TransformSeparatorTest.java          | 169 +++-
 .../transform/TranslationTransformTest.java        | 113 +++
 .../report/CoordinateOperationMethods.java         |   2 +-
 .../sis/test/suite/ReferencingTestSuite.java       |   4 +-
 .../org/apache/sis/referencing/crs/DerivedCRS.xml  |   2 +-
 .../sis/internal/converter/ConverterRegistry.java  |   2 +-
 .../sis/internal/converter/SystemRegistry.java     |   2 +-
 .../java/org/apache/sis/internal/jdk9/JDK9.java    |  25 +-
 .../apache/sis/internal/system/DaemonThread.java   |   4 +-
 .../org/apache/sis/internal/system/Threads.java    |   4 +-
 .../sis/internal/util/AutoMessageFormat.java       | 114 +++
 .../apache/sis/internal/util/CollectionsExt.java   |   2 +
 .../org/apache/sis/internal/util/Constants.java    |  16 +-
 .../org/apache/sis/internal/util/DoubleDouble.java |   4 +-
 .../sis/internal/util/ListOfUnknownSize.java       | 263 ++++++
 .../org/apache/sis/internal/util/Numerics.java     |  49 --
 .../apache/sis/internal/util/SetOfUnknownSize.java |  24 +-
 .../sis/internal/util/StandardDateFormat.java      |  95 +-
 .../sis/internal/util/TemporalUtilities.java       |  10 +
 .../sis/internal/util/UnmodifiableArrayList.java   |   4 +-
 .../java/org/apache/sis/internal/util/X364.java    |   3 +-
 .../main/java/org/apache/sis/io/TableAppender.java |  23 +-
 .../main/java/org/apache/sis/io/TabularFormat.java |   2 +-
 .../main/java/org/apache/sis/math/ArrayVector.java | 220 ++++-
 .../java/org/apache/sis/math/MathFunctions.java    |  59 +-
 .../src/main/java/org/apache/sis/math/Plane.java   |   2 +-
 .../java/org/apache/sis/math/RepeatedVector.java   | 270 ++++++
 .../java/org/apache/sis/math/SequenceVector.java   |  66 +-
 .../src/main/java/org/apache/sis/math/Vector.java  | 475 +++++++++-
 .../java/org/apache/sis/measure/AbstractUnit.java  |  32 +-
 .../main/java/org/apache/sis/measure/Angle.java    |   2 +-
 .../org/apache/sis/measure/MeasurementRange.java   |  11 +
 .../java/org/apache/sis/measure/NumberRange.java   |  77 +-
 .../main/java/org/apache/sis/measure/Range.java    |  21 +-
 .../java/org/apache/sis/measure/RangeFormat.java   |  26 +-
 .../main/java/org/apache/sis/measure/Salinity.java |   2 +-
 .../java/org/apache/sis/measure/UnitDimension.java |   3 +-
 .../java/org/apache/sis/measure/UnitFormat.java    |  26 +-
 .../main/java/org/apache/sis/measure/Units.java    |  11 +-
 .../java/org/apache/sis/measure/ValueRange.java    |   4 +-
 .../main/java/org/apache/sis/setup/OptionKey.java  |   2 +-
 .../java/org/apache/sis/setup/package-info.java    |   2 +-
 .../main/java/org/apache/sis/util/ArraysExt.java   | 252 +++++-
 .../src/main/java/org/apache/sis/util/Classes.java |   6 +-
 .../src/main/java/org/apache/sis/util/Numbers.java |   9 +-
 .../java/org/apache/sis/util/collection/Cache.java |   2 +-
 .../org/apache/sis/util/collection/Containers.java |  16 +-
 .../sis/util/collection/TreeTableFormat.java       |  35 +-
 .../sis/util/collection/WeakValueHashMap.java      |   2 +-
 .../sis/util/iso/ResourceInternationalString.java  |   2 +-
 .../org/apache/sis/util/logging/LoggerAdapter.java |   2 +-
 .../apache/sis/util/logging/MonolineFormatter.java |  11 +-
 .../apache/sis/util/logging/QuietLogRecord.java    |   2 +-
 .../java/org/apache/sis/util/resources/Errors.java |   4 +-
 .../apache/sis/util/resources/Errors.properties    |   4 +-
 .../apache/sis/util/resources/Errors_fr.properties |   4 +-
 .../sis/util/resources/IndexedResourceBundle.java  |  15 +-
 .../org/apache/sis/util/resources/Vocabulary.java  |  45 +
 .../sis/util/resources/Vocabulary.properties       |   9 +
 .../sis/util/resources/Vocabulary_fr.properties    |   9 +
 .../apache/sis/internal/util/DoubleDoubleTest.java |   4 +-
 .../sis/internal/util/ListOfUnknownSizeTest.java   |  60 ++
 .../org/apache/sis/internal/util/NumericsTest.java |   9 -
 .../sis/internal/util/StandardDateFormatTest.java  |  39 +-
 .../org/apache/sis/math/MathFunctionsTest.java     |  25 +-
 .../org/apache/sis/math/RepeatedVectorTest.java    | 172 ++++
 .../test/java/org/apache/sis/math/VectorTest.java  |  32 +-
 .../apache/sis/measure/ConventionalUnitTest.java   |  49 +-
 .../org/apache/sis/measure/UnitFormatTest.java     |  14 +-
 .../org/apache/sis/test/TestConfiguration.java     |   7 +-
 .../test/java/org/apache/sis/test/TestStep.java    |   2 +-
 .../test/java/org/apache/sis/test/TestSuite.java   |   9 +-
 .../apache/sis/test/suite/UtilityTestSuite.java    |   4 +-
 .../java/org/apache/sis/util/ArraysExtTest.java    |  24 +-
 .../org/apache/sis/util/collection/CacheTest.java  |   3 +-
 .../sis/util/collection/TreeTableFormatTest.java   |  33 +-
 ide-project/NetBeans/nbproject/genfiles.properties |   2 +-
 ide-project/NetBeans/nbproject/project.properties  |   2 +
 ide-project/NetBeans/nbproject/project.xml         |   7 +
 pom.xml                                            |   6 +-
 .../sis/test/suite/FrenchProfileTestSuite.java     |   2 +-
 storage/pom.xml                                    |   1 +
 .../storage/earthobservation/LandsatReader.java    |   2 +-
 .../sis/storage/earthobservation/LandsatStore.java |   2 +-
 .../doc-files/LandsatMetadata.html                 |   8 +-
 .../sis/test/suite/EarthObservationTestSuite.java  |   2 +-
 .../src/main/c/org_apache_sis_storage_gdal_PJ.c    |   2 +-
 .../org/apache/sis/storage/gdal/Proj4Factory.java  |   6 +-
 .../org/apache/sis/storage/gdal/package-info.java  |   2 +-
 .../org/apache/sis/test/suite/GDALTestSuite.java   |   2 +-
 .../org/apache/sis/storage/geotiff/CRSBuilder.java | 142 +--
 .../apache/sis/storage/geotiff/GeoTiffStore.java   |  92 +-
 .../sis/storage/geotiff/ImageFileDirectory.java    |  61 +-
 .../apache/sis/test/suite/GeoTiffTestSuite.java    |   2 +-
 .../java/org/apache/sis/internal/netcdf/Axis.java  | 381 +++++++-
 .../org/apache/sis/internal/netcdf/CRSBuilder.java | 751 ++++++++++++++++
 .../org/apache/sis/internal/netcdf/Decoder.java    |  27 +-
 .../java/org/apache/sis/internal/netcdf/Grid.java  | 412 +++++++++
 .../apache/sis/internal/netcdf/GridGeometry.java   |  87 --
 .../apache/sis/internal/netcdf/NamedElement.java   |  96 +-
 .../org/apache/sis/internal/netcdf/Resources.java  |  23 +
 .../sis/internal/netcdf/Resources.properties       |   4 +
 .../sis/internal/netcdf/Resources_fr.properties    |   4 +
 .../org/apache/sis/internal/netcdf/Variable.java   | 604 ++++++++++++-
 .../sis/internal/netcdf/impl/ChannelDecoder.java   | 149 +++-
 .../sis/internal/netcdf/impl/FeaturesInfo.java     |  34 +-
 .../impl/{GridGeometryInfo.java => GridInfo.java}  | 161 +++-
 .../org/apache/sis/internal/netcdf/impl/HYCOM.java | 129 +++
 .../sis/internal/netcdf/impl/VariableInfo.java     | 279 ++++--
 .../sis/internal/netcdf/ucar/DecoderWrapper.java   |  32 +-
 .../sis/internal/netcdf/ucar/FeaturesWrapper.java  |   6 -
 .../{GridGeometryWrapper.java => GridWrapper.java} |  83 +-
 .../sis/internal/netcdf/ucar/VariableWrapper.java  | 235 ++++-
 .../apache/sis/storage/netcdf/AttributeNames.java  |   4 +-
 .../apache/sis/storage/netcdf/GridResource.java    | 416 +++++++++
 .../java/org/apache/sis/storage/netcdf/Image.java  |  77 ++
 .../apache/sis/storage/netcdf/MetadataReader.java  | 128 +--
 .../org/apache/sis/storage/netcdf/NetcdfStore.java |  15 +-
 .../sis/storage/netcdf/NetcdfStoreProvider.java    |   4 +-
 .../{GridGeometryTest.java => GridTest.java}       |  27 +-
 .../apache/sis/internal/netcdf/VariableTest.java   |  16 +-
 ...GridGeometryInfoTest.java => GridInfoTest.java} |  22 +-
 .../sis/storage/netcdf/MetadataReaderTest.java     |   1 +
 .../org/apache/sis/test/suite/NetcdfTestSuite.java |   6 +-
 .../apache/sis/test/suite/ShapefileTestSuite.java  |   2 +-
 .../org/apache/sis/internal/sql/feature/Table.java |   4 +-
 .../org/apache/sis/test/suite/SQLTestSuite.java    |   2 +-
 .../sis/internal/storage/AbstractFeatureSet.java   |  82 +-
 .../sis/internal/storage/AbstractGridResource.java | 116 +++
 .../sis/internal/storage/AbstractResource.java     | 150 ++--
 .../sis/internal/storage/MemoryFeatureSet.java     |  28 +-
 .../sis/internal/storage/MetadataBuilder.java      |  75 +-
 .../org/apache/sis/internal/storage/Resources.java |   5 +
 .../sis/internal/storage/Resources.properties      |   1 +
 .../sis/internal/storage/Resources_fr.properties   |   1 +
 .../sis/internal/storage/StoreUtilities.java       |  41 +-
 .../sis/internal/storage/csv/package-info.java     |   2 +-
 .../apache/sis/internal/storage/folder/Store.java  |   3 +
 .../sis/internal/storage/io/ChannelFactory.java    |  15 +-
 .../sis/internal/storage/query/FeatureSubset.java  |   9 -
 .../sis/internal/storage/wkt/FirstKeywordPeek.java |   2 +-
 .../sis/internal/storage/xml/AbstractProvider.java |   2 +-
 .../internal/storage/xml/GeographicEnvelope.java   |   2 +-
 .../java/org/apache/sis/storage/Aggregate.java     |   2 +-
 .../main/java/org/apache/sis/storage/DataSet.java  |   5 +-
 .../java/org/apache/sis/storage/DataStore.java     |  34 +-
 .../java/org/apache/sis/storage/FeatureNaming.java |   2 +-
 .../apache/sis/storage/GridCoverageResource.java   |  60 +-
 .../org/apache/sis/storage/StorageConnector.java   |  17 +-
 .../org/apache/sis/storage/WritableAggregate.java  |   2 +-
 .../internal/storage/query/SimpleQueryTest.java    |   2 +-
 .../apache/sis/test/suite/StorageTestSuite.java    |   2 +-
 .../apache/sis/internal/storage/gpx/Metadata.java  |   3 +-
 .../org/apache/sis/internal/storage/gpx/Store.java |   4 +-
 .../sis/internal/storage/gpx/WriterTest.java       |   3 +-
 .../org/apache/sis/test/suite/GPXTestSuite.java    |   2 +-
 429 files changed, 17797 insertions(+), 3076 deletions(-)

diff --cc core/sis-feature/src/main/java/org/apache/sis/feature/DefaultFeatureType.java
index e017104,a1007cf..6d44383
--- 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
@@@ -831,12 -839,8 +831,12 @@@ public class DefaultFeatureType extend
       * inherited from the {@linkplain #getSuperTypes() super-types} only if {@code includeSuperTypes}
       * is {@code true}.
       *
 +     * <div class="warning"><b>Warning:</b>
 +     * The type of list elements will be changed to {@code PropertyType} if and when such interface
 +     * will be defined in GeoAPI.</div>
 +     *
       * @param  includeSuperTypes  {@code true} for including the properties inherited from the super-types,
-      *         or {@code false} for returning only the properties defined explicitely in this type.
+      *         or {@code false} for returning only the properties defined explicitly in this type.
       * @return feature operation, attribute type and association role that carries characteristics of this
       *         feature type (not including parent types).
       */
diff --cc core/sis-feature/src/main/java/org/apache/sis/feature/Validator.java
index 87dc23b,20697e8..bb8933e
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/Validator.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/Validator.java
@@@ -106,12 -116,12 +106,12 @@@ final class Validator 
      /**
       * Implementation of {@link AbstractFeature#quality()}, also shared by {@link Features} static method.
       *
-      * @param type     the type of the {@code feature} argument, provided explicitely for protecting from user overriding.
+      * @param type     the type of the {@code feature} argument, provided explicitly for protecting from user overriding.
       * @param feature  the feature to validate.
       */
 -    void validate(final FeatureType type, final Feature feature) {
 -        for (final PropertyType pt : type.getProperties(true)) {
 -            final Property property = feature.getProperty(pt.getName().toString());
 +    void validate(final FeatureType type, final AbstractFeature feature) {
 +        for (final AbstractIdentifiedType pt : type.getProperties(true)) {
 +            final Object property = feature.getProperty(pt.getName().toString());
              final DataQuality pq;
              if (property instanceof AbstractAttribute<?>) {
                  pq = ((AbstractAttribute<?>) property).quality();
diff --cc core/sis-feature/src/main/java/org/apache/sis/feature/builder/FeatureTypeBuilder.java
index fc5c6d7,bb227ba..359c68c
--- 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
@@@ -203,15 -205,13 +203,15 @@@ public class FeatureTypeBuilder extend
       * This constructor initializes the list of {@linkplain #properties() properties}, the
       * {@linkplain #getSuperTypes() super types} and {@link #isAbstract() isAbstract} flag
       * to values inferred from the given template. The properties list will contain properties
-      * declared explicitely in the given template, not including properties inherited from super types.
+      * declared explicitly in the given template, not including properties inherited from super types.
       *
 -     * @param template  an existing feature type to use as a template, or {@code null} if none.
 +     * <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>
       *
 -     * @see #setAll(FeatureType)
 +     * @param template  an existing feature type to use as a template, or {@code null} if none.
       */
 -    public FeatureTypeBuilder(final FeatureType template) {
 +    public FeatureTypeBuilder(final DefaultFeatureType template) {
          this(null, null, null);
          if (template != null) {
              initialize(template);
diff --cc core/sis-feature/src/test/java/org/apache/sis/feature/CharacteristicMapTest.java
index dd75f3e,841275e..c40f504
--- a/core/sis-feature/src/test/java/org/apache/sis/feature/CharacteristicMapTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/feature/CharacteristicMapTest.java
@@@ -58,7 -61,7 +58,7 @@@ public final strictfp class Characteris
      }
  
      /**
-      * Tests adding explicitely a characteristic with {@code CharacteristicMap.put(String, Attribute)}.
 -     * Tests adding explicitly a characteristic with {@link CharacteristicMap#put(String, Attribute)}.
++     * Tests adding explicitly a characteristic with {@code CharacteristicMap.put(String, Attribute)}.
       */
      @Test
      public void testPut() {
diff --cc core/sis-metadata/src/main/java/org/apache/sis/io/wkt/Formatter.java
index baabd2c,2089d58..05e160a
--- a/core/sis-metadata/src/main/java/org/apache/sis/io/wkt/Formatter.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/io/wkt/Formatter.java
@@@ -1485,13 -1488,44 +1489,42 @@@ public class Formatter implements Local
              } else {
                  append(number.doubleValue());
              }
 -        } else if (value instanceof ControlledVocabulary) {
 -            append((ControlledVocabulary) value);
 -        } else if (value instanceof Date) {
 -            append((Date) value);
 -        } else if (value instanceof Boolean) {
 -            append((Boolean) value);
 -        } else if (value instanceof CharSequence) {
 +        }
 +        else if (value instanceof CodeList<?>) append((CodeList<?>) value);
 +        else if (value instanceof Date)        append((Date)        value);
 +        else if (value instanceof Boolean)     append((Boolean)     value);
 +        else if (value instanceof CharSequence) {
              append((value instanceof InternationalString) ?
                      ((InternationalString) value).toString(locale) : value.toString(), null);
+         } else if (value.getClass().isArray()) {
+             /*
+              * All above cases delegated to another method which invoke 'appendSeparator()'.
+              * Since the following block is writing itself a new element, we need to invoke
+              * 'appendSeparator()' here. This block invokes (indirectly) this 'appendValue'
+              * method recursively for some or all elements in the list.
+              */
+             appendSeparator();
+             elementStart = buffer.appendCodePoint(symbols.getOpenSequence()).length();
+             final int length = Array.getLength(value);
+             final int cut = (length <= listSizeLimit) ? length : Math.max(listSizeLimit/2 - 1, 1);
+             for (int i=0; i<length; i++) {
+                 if (i == cut) {
+                     /*
+                      * Skip elements in the middle if the list is too long. The 'cut' index has been computed
+                      * in such a way that the number of elements to skip should be greater than 1, otherwise
+                      * formatting the single missing element would often have been shorter.
+                      */
+                     final int skip = length - Math.min(2*cut, listSizeLimit);
+                     buffer.append(symbols.getSeparator());
+                     setColor(ElementKind.REMARKS);
+                     buffer.append(Resources.forLocale(locale).getString(Resources.Keys.ElementsOmitted_1, skip));
+                     resetColor();
+                     i += skip;
+                     setInvalidWKT(value.getClass().getSimpleName(), null);
+                 }
+                 appendAny(Array.get(value, i));
+             }
+             buffer.appendCodePoint(symbols.getCloseSequence());
          } else {
              return false;
          }
diff --cc core/sis-raster/src/main/java/org/apache/sis/coverage/SampleDimension.java
index 0000000,6294919..09b2507
mode 000000,100644..100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleDimension.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleDimension.java
@@@ -1,0 -1,970 +1,967 @@@
+ /*
+  * 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.coverage;
+ 
+ import java.util.List;
+ import java.util.ArrayList;
+ import java.util.Set;
+ import java.util.TreeSet;
+ import java.util.Optional;
+ import java.util.Collection;
+ import java.util.Collections;
+ import java.util.Locale;
+ import java.util.Objects;
+ import java.io.Serializable;
+ import javax.measure.Unit;
+ import org.opengis.util.GenericName;
+ import org.opengis.util.InternationalString;
+ import org.opengis.referencing.operation.MathTransform1D;
+ import org.apache.sis.referencing.operation.transform.TransferFunction;
+ import org.apache.sis.internal.raster.Resources;
+ import org.apache.sis.measure.MeasurementRange;
+ import org.apache.sis.measure.NumberRange;
+ import org.apache.sis.util.resources.Vocabulary;
+ import org.apache.sis.util.ArgumentChecks;
+ import org.apache.sis.util.iso.Names;
+ import org.apache.sis.util.Numbers;
+ import org.apache.sis.util.Debug;
+ 
+ 
+ /**
+  * Describes the data values in a coverage (the range). For a raster, a sample dimension is a band.
+  * A sample dimension can reserve some values for <cite>qualitative</cite> information like  “this
+  * is a forest” and some other values for <cite>quantitative</cite> information like a temperature
+  * measurements.
+  *
+  * <div class="note"><b>Example:</b>
+  * an image of sea surface temperature (SST) could define the following categories:
+  * <table class="sis">
+  *   <caption>Example of categories in a sample dimension</caption>
+  *   <tr><th>Values range</th> <th>Meaning</th></tr>
+  *   <tr><td>[0]</td>          <td>No data</td></tr>
+  *   <tr><td>[1]</td>          <td>Cloud</td></tr>
+  *   <tr><td>[2]</td>          <td>Land</td></tr>
+  *   <tr><td>[10…210]</td>     <td>Temperature to be converted into Celsius degrees through a linear equation</td></tr>
+  * </table>
+  * In this example, sample values in range [10…210] define a quantitative category, while all others categories are qualitative.
+  * </div>
+  *
+  * <div class="section">Relationship with metadata</div>
 - * This class provides the same information than ISO 19115 {@link org.opengis.metadata.content.SampleDimension},
++ * This class provides the same information than ISO 19115 {@code org.opengis.metadata.content.SampleDimension},
+  * but organized in a different way. The use of the same name may seem a risk, but those two types are typically
+  * not used in same time.
+  *
+  * @author  Martin Desruisseaux (IRD, Geomatys)
+  * @version 1.0
 - *
 - * @see org.opengis.metadata.content.SampleDimension
 - *
+  * @since 1.0
+  * @module
+  */
+ public class SampleDimension implements Serializable {
+     /**
+      * Serial number for inter-operability with different versions.
+      */
+     private static final long serialVersionUID = 6026936545776852758L;
+ 
+     /**
+      * Identification for this sample dimension. Typically used as a way to perform a band select by
+      * using human comprehensible descriptions instead of just numbers. Web Coverage Service (WCS)
+      * can use this name in order to perform band sub-setting as directed from a user request.
+      *
+      * @see #getName()
+      */
+     private final GenericName name;
+ 
+     /**
+      * The background value, or {@code null} if unspecified. Should be a sample value of
+      * a qualitative category in the {@link #categories} list, but this is not mandatory.
+      *
+      * @see #getBackground()
+      */
+     private final Number background;
+ 
+     /**
+      * The list of categories making this sample dimension. May be empty but shall never be null.
+      */
+     private final CategoryList categories;
+ 
+     /**
+      * The transform from samples to real values. May be {@code null} if this sample dimension
+      * does not define any transform (which is not the same that defining an identity transform).
+      *
+      * @see #getTransferFunction()
+      */
+     private final MathTransform1D transferFunction;
+ 
+     /**
+      * The {@code SampleDimension} that describes values after {@linkplain #getTransferFunction() transfer function}
+      * has been applied, or if this {@code SampleDimension} is already converted then the original sample dimension.
+      * May be {@code null} if this sample dimension has no transfer function, or {@code this} if the transfer function
+      * is the identity function.
+      *
+      * <p>This field establishes a bidirectional navigation between sample values and real values.
+      * This is in contrast with methods named {@link #converted()}, which establish a unidirectional
+      * navigation from sample values to real values.</p>
+      *
+      * @see #converted()
+      * @see Category#converse
+      * @see CategoryList#converse
+      */
+     private final SampleDimension converse;
+ 
+     /**
+      * Creates a new sample dimension for values that are already converted to real values.
+      * This transfer function is set to identity, which implies that this constructor should
+      * be invoked only for sample dimensions having at least one quantitative category.
+      *
+      * @param  original  the original sample dimension for packed values.
+      * @param  bc        category of the background value in original sample dimension, or {@code null}.
+      */
+     private SampleDimension(final SampleDimension original, Category bc) {
+         converse         = original;
+         name             = original.name;
+         categories       = original.categories.converse;
+         transferFunction = Category.identity();
+         assert hasQuantitative();
+         if (bc == null) {
+             background = null;
+         } else {
+             bc = bc.converse;
+             final NumberRange<?> range = bc.range;
+             if (range != null) {
+                 background = range.getMinValue();
+             } else {
+                 background = (float) bc.minimum;
+             }
+         }
+     }
+ 
+     /**
+      * Creates a sample dimension with the specified name and categories.
+      * The sample dimension name is used as a way to perform a band select
+      * by using human comprehensible descriptions instead of numbers.
+      * The background value is used for filling empty space in map reprojections.
+      * The background value (if specified) should be the value of a qualitative category
+      * present in the {@code categories} collection, but this is not mandatory.
+      *
+      * <p>Note that {@link Builder} provides a more convenient way to create sample dimensions.</p>
+      *
+      * @param name        an identification for the sample dimension.
+      * @param background  the background value, or {@code null} if none.
+      * @param categories  the list of categories. May be empty if none.
+      */
+     public SampleDimension(final GenericName name, final Number background, final Collection<? extends Category> categories) {
+         ArgumentChecks.ensureNonNull("name", name);
+         ArgumentChecks.ensureNonNull("categories", categories);
+         final CategoryList list;
+         if (categories.isEmpty()) {
+             list = CategoryList.EMPTY;
+         } else {
+             list = new CategoryList(categories.toArray(new Category[categories.size()]), null);
+         }
+         this.name       = name;
+         this.background = background;
+         this.categories = list;
+         if (list.converse.range == null) {      // !hasQuantitative() inlined since we can not yet invoke that method.
+             transferFunction = null;
+             converse = null;
+         } else if (list == list.converse) {
+             transferFunction = Category.identity();
+             converse = this;
+         } else {
+             assert !list.isEmpty();             // Verified by inlined !hasQuantitative() above.
+             transferFunction = list.getTransferFunction();
+             converse = new SampleDimension(this, (background != null) ? list.search(background.doubleValue()) : null);
+         }
+     }
+ 
+     /**
+      * Returns the sample dimension that describes real values. This method establishes a unidirectional navigation
+      * from sample values to real values. This is in contrast to {@link #converse}, which establish a bidirectional
+      * navigation.
+      *
+      * @see #forConvertedValues(boolean)
+      */
+     private SampleDimension converted() {
+         // Transfer function shall never be null if 'converse' is non-null.
+         return (converse != null && !transferFunction.isIdentity()) ? converse : this;
+     }
+ 
+     /**
+      * Returns an identification for this sample dimension. This is typically used as a way to perform a band select
+      * by using human comprehensible descriptions instead of just numbers. Web Coverage Service (WCS) can use this name
+      * in order to perform band sub-setting as directed from a user request.
+      *
+      * @return an identification of this sample dimension.
+      *
+      * @see org.opengis.metadata.content.RangeDimension#getSequenceIdentifier()
+      */
+     public GenericName getName() {
+         return name;
+     }
+ 
+     /**
+      * Returns all categories in this sample dimension. Note that a {@link Category} object may apply to an arbitrary range
+      * of sample values. Consequently, the first element in this collection may not be directly related to the sample value
+      * {@code 0}.
+      *
+      * @return the list of categories in this sample dimension, or an empty list if none.
+      */
+     @SuppressWarnings("ReturnOfCollectionOrArrayField")
+     public List<Category> getCategories() {
+         return categories;                      // Safe to return because immutable.
+     }
+ 
+     /**
+      * Returns the background value. If this sample dimensions has quantitative categories, then the background
+      * value should be one of the value returned by {@link #getNoDataValues()}. However this is not mandatory.
+      *
+      * @return the background value.
+      */
+     public Optional<Number> getBackground() {
+         return Optional.ofNullable(background);
+     }
+ 
+     /**
+      * Returns {@code true} if this list contains at least one quantitative category.
+      * We use the converted range has a criterion, since it shall be null if the result
+      * of all conversions is NaN.
+      *
+      * @see Category#isQuantitative()
+      */
+     private boolean hasQuantitative() {
+         return converted().categories.range != null;
+     }
+ 
+     /**
+      * Returns the values to indicate "no data" for this sample dimension.
+      *
+      * @return the values to indicate no data values for this sample dimension, or an empty set if none.
+      * @throws IllegalStateException if this method can not expand the range of no data values, for example
+      *         because some ranges contain an infinite amount of values.
+      */
+     public Set<Number> getNoDataValues() {
+         if (hasQuantitative()) {
+             final NumberRange<?>[] ranges = new NumberRange<?>[categories.size()];
+             Class<? extends Number> widestClass = Byte.class;
+             int count = 0;
+             for (final Category category : categories) {
+                 final NumberRange<?> range = category.range;
+                 if (range != null && !category.isQuantitative()) {
+                     if (!range.isBounded()) {
+                         throw new IllegalStateException(Resources.format(Resources.Keys.CanNotEnumerateValuesInRange_1, range));
+                     }
+                     widestClass = Numbers.widestClass(widestClass, range.getElementType());
+                     ranges[count++] = range;
+                 }
+             }
+             if (count != 0) {
+                 final Set<Number> noDataValues = new TreeSet<>();
+                 for (int i=0; i<count; i++) {
+                     final NumberRange<?> range = ranges[i];
+                     final Number minimum = range.getMinValue();
+                     final Number maximum = range.getMaxValue();
+                     if (range.isMinIncluded()) noDataValues.add(Numbers.cast(minimum, widestClass));
+                     if (range.isMaxIncluded()) noDataValues.add(Numbers.cast(maximum, widestClass));
+                     if (Numbers.isInteger(range.getElementType())) {
+                         long value = minimum.longValue() + 1;       // If value was inclusive, then it has already been added to the set.
+                         long stop  = maximum.longValue() - 1;
+                         while (value <= stop) {
+                             noDataValues.add(Numbers.wrap(value, widestClass));
+                         }
+                     } else if (!minimum.equals(maximum)) {
+                         throw new IllegalStateException(Resources.format(Resources.Keys.CanNotEnumerateValuesInRange_1, range));
+                     }
+                 }
+                 return noDataValues;
+             }
+         }
+         return Collections.emptySet();
+     }
+ 
+     /**
+      * Returns the range of values occurring in this sample dimension. The range delimits sample values that
+      * can be converted into real values using the {@linkplain #getTransferFunction() transfer function}.
+      * If that function is {@linkplain MathTransform1D#isIdentity() identity}, then the values are already
+      * real values and the range may be an instance of {@link MeasurementRange}
+      * (i.e. a number range with units of measurement).
+      *
+      * @return the range of sample values in this sample dimension.
+      */
+     public Optional<NumberRange<?>> getSampleRange() {
+         return Optional.ofNullable(categories.range);
+     }
+ 
+     /**
+      * Returns the range of values after conversions by the transfer function.
+      * This range is absent if there is no transfer function.
+      *
+      * @return the range of values after conversion by the transfer function.
+      *
+      * @see #getUnits()
+      */
+     public Optional<MeasurementRange<?>> getMeasurementRange() {
+         // A ClassCastException below would be a bug in our constructors.
+         return Optional.ofNullable((MeasurementRange<?>) converted().categories.range);
+     }
+ 
+     /**
+      * Returns the <cite>transfer function</cite> from sample values to real values.
+      * This method returns a transform expecting sample values as input and computing real values as output.
+      * The output units of measurement is given by {@link #getUnits()}.
+      *
+      * <p>This transform takes care of converting all "{@linkplain #getNoDataValues() no data values}" into {@code NaN} values.
+      * The <code>transferFunction.{@linkplain MathTransform1D#inverse() inverse()}</code> transform is capable to differentiate
+      * those {@code NaN} values and get back the original sample value.</p>
+      *
+      * @return the <cite>transfer function</cite> from sample to real values. May be absent if this sample dimension
+      *         does not define any transform (which is not the same that defining an identity transform).
+      */
+     public Optional<MathTransform1D> getTransferFunction() {
+         return Optional.ofNullable(transferFunction);
+     }
+ 
+     /**
+      * Returns the scale factor and offset of the transfer function.
+      * The formula returned by this method does <strong>not</strong> take
+      * "{@linkplain #getNoDataValues() no data values}" in account.
+      * For a more generic transfer function, see {@link #getTransferFunction()}.
+      *
+      * @return a description of the part of the transfer function working on real numbers.
+      * @throws IllegalStateException if the transfer function can not be simplified in a form representable
+      *         by {@link TransferFunction}.
+      */
+     public Optional<TransferFunction> getTransferFunctionFormula() {
+         MathTransform1D tr = null;
+         for (final Category category : categories) {
+             final Optional<MathTransform1D> c = category.getTransferFunction();
+             if (c.isPresent()) {
+                 if (tr == null) {
+                     tr = c.get();
+                 } else if (!tr.equals(c.get())) {
+                     throw new IllegalStateException(Resources.format(Resources.Keys.CanNotSimplifyTransferFunction_1));
+                 }
+             }
+         }
+         if (tr == null) {
+             return Optional.empty();
+         }
+         final TransferFunction f = new TransferFunction();
+         try {
+             f.setTransform(tr);
+         } catch (IllegalArgumentException e) {
+             throw new IllegalStateException(Resources.format(Resources.Keys.CanNotSimplifyTransferFunction_1, e));
+         }
+         return Optional.of(f);
+     }
+ 
+     /**
+      * Returns the units of measurement for this sample dimension.
+      * This unit applies to values obtained after the {@linkplain #getTransferFunction() transfer function}.
+      * May be absent if not applicable.
+      *
+      * @return the units of measurement.
+      * @throws IllegalStateException if this sample dimension use different units.
+      *
+      * @see #getMeasurementRange()
+      */
+     public Optional<Unit<?>> getUnits() {
+         Unit<?> unit = null;
+         final SampleDimension converted = converted();
+         for (final Category category : converted.categories) {
+             final NumberRange<?> r = category.range;
+             if (r instanceof MeasurementRange<?>) {
+                 final Unit<?> c = ((MeasurementRange<?>) r).unit();
+                 if (c != null) {
+                     if (unit == null) {
+                         unit = c;
+                     } else if (!unit.equals(c)) {
+                         throw new IllegalStateException();
+                     }
+                 }
+             }
+         }
+         return Optional.ofNullable(unit);
+     }
+ 
+     /**
+      * Returns a sample dimension that describes real values or sample values, depending if {@code converted} is {@code true}
+      * or {@code false} respectively.  If there is no {@linkplain #getTransferFunction() transfer function}, then this method
+      * returns {@code this}.
+      *
+      * @param  converted  {@code true} for a sample dimension representing converted values,
+      *                    or {@code false} for a sample dimension representing sample values.
+      * @return a sample dimension representing converted or sample values, depending on {@code converted} argument value.
+      *         May be {@code this} but never {@code null}.
+      */
+     public SampleDimension forConvertedValues(final boolean converted) {
+         // Transfer function shall never be null if 'converse' is non-null.
+         if (converse != null && transferFunction.isIdentity() != converted) {
+             return converse;
+         }
+         return this;
+     }
+ 
+     /**
+      * Returns a hash value for this sample dimension.
+      */
+     @Override
+     public int hashCode() {
+         return categories.hashCode() + 31*name.hashCode();
+     }
+ 
+     /**
+      * Compares the specified object with this sample dimension for equality.
+      *
+      * @param  object  the object to compare with.
+      * @return {@code true} if the given object is equals to this sample dimension.
+      */
+     @Override
+     public boolean equals(final Object object) {
+         if (object == this) {
+             return true;
+         }
+         if (object instanceof SampleDimension) {
+             final SampleDimension that = (SampleDimension) object;
+             return name.equals(that.name) && Objects.equals(background, that.background) && categories.equals(that.categories);
+         }
+         return false;
+     }
+ 
+     /**
+      * Returns a string representation of this sample dimension.
+      * This string is for debugging purpose only and may change in future version.
+      *
+      * @return a string representation of this sample dimension for debugging purpose.
+      */
+     @Override
+     public String toString() {
+         return new SampleRangeFormat(Locale.getDefault()).write(new SampleDimension[] {this});
+     }
+ 
+     /**
+      * Returns a string representation of the given sample dimensions.
+      * This string is for debugging purpose only and may change in future version.
+      *
+      * @param  locale      the locale to use for formatting texts.
+      * @param  dimensions  the sample dimensions to format.
+      * @return a string representation of the given sample dimensions for debugging purpose.
+      */
+     @Debug
+     public static String toString(final Locale locale, SampleDimension... dimensions) {
+         ArgumentChecks.ensureNonNull("dimensions", dimensions);
+         return new SampleRangeFormat(locale).write(dimensions);
+     }
+ 
+ 
+ 
+ 
+     /**
+      * A mutable builder for creating an immutable {@link SampleDimension}.
+      * The following properties can be set:
+      *
+      * <ul>
+      *   <li>An optional name for the {@code SampleDimension}.</li>
+      *   <li>A single optional category for the background value.</li>
+      *   <li>An arbitrary amount of <cite>qualitative</cite> categories.</li>
+      *   <li>An arbitrary amount of <cite>quantitative</cite> categories.</li>
+      * </ul>
+      *
+      * A <cite>qualitative category</cite> is a range of sample values associated to a label (not numbers).
+      * For example 0 = cloud, 1 = sea, 2 = land, <i>etc</i>.
+      * A <cite>quantitative category</cite> is a range of sample values associated to numbers with units of measurement.
+      * For example 10 = 1.0°C, 11 = 1.1°C, 12 = 1.2°C, <i>etc</i>.
+      * Those three kinds of category are created by the following methods:
+      *
+      * <ul>
+      *   <li>{@link #setBackground(CharSequence, Number)}</li>
+      *   <li>{@link #addQualitative(CharSequence, NumberRange)}</li>
+      *   <li>{@link #addQuantitative(CharSequence, NumberRange, MathTransform1D, Unit)}</li>
+      * </ul>
+      *
+      * All other {@code addQualitative(…)} and {@code addQuantitative(…)} methods are convenience methods delegating
+      * to above-cited methods. Qualitative and quantitative categories can be mixed in the same {@link SampleDimension},
+      * provided that their ranges do not overlap.
+      * After properties have been set, the sample dimension is created by invoking {@link #build()}.
+      *
+      * @author  Martin Desruisseaux (IRD, Geomatys)
+      * @version 1.0
+      * @since   1.0
+      * @module
+      */
+     public static class Builder {
+         /**
+          * Identification for this sample dimension.
+          */
+         private GenericName dimensionName;
+ 
+         /**
+          * The background value, or {@code null} if unspecified.
+          */
+         private Number background;
+ 
+         /**
+          * The categories for this sample dimension.
+          */
+         private final List<Category> categories;
+ 
+         /**
+          * The ordinal NaN values used for this sample dimension.
+          * The {@link Category} constructor uses this set for avoiding collisions.
+          */
+         private final ToNaN toNaN;
+ 
+         /**
+          * Creates an initially empty builder for a sample dimension.
+          * Callers shall invoke at least one {@code addFoo(…)} method before {@link #build()}.
+          */
+         public Builder() {
+             categories = new ArrayList<>();
+             toNaN      = new ToNaN();
+         }
+ 
+         /**
+          * Sets an identification of the sample dimension.
+          * This is the value to be returned by {@link SampleDimension#getName()}.
+          * If this method is invoked more than once, then the last specified name prevails
+          * (previous sample dimension names are discarded).
+          *
+          * @param  name  identification of the sample dimension.
+          * @return {@code this}, for method call chaining.
+          */
+         public Builder setName(final GenericName name) {
+             dimensionName = name;
+             return this;
+         }
+ 
+         /**
+          * Sets an identification of the sample dimension as a character sequence.
+          * This is a convenience method for creating a {@link GenericName} from the given characters.
+          *
+          * @param  name  identification of the sample dimension.
+          * @return {@code this}, for method call chaining.
+          */
+         public Builder setName(final CharSequence name) {
+             dimensionName = createLocalName(name);
+             return this;
+         }
+ 
+         /**
+          * Sets an identification of the sample dimension as a band number.
+          * This method should be used only when no more descriptive name is available.
+          *
+          * @param  band  sequence identifier of the sample dimension to create.
+          * @return {@code this}, for method call chaining.
+          */
+         public Builder setName(final int band) {
+             dimensionName = Names.createMemberName(null, null, band);
+             return this;
+         }
+ 
+         /**
+          * A common place where are created local names from character string.
+          * For making easier to revisit if we want to add a namespace.
+          */
+         private static GenericName createLocalName(final CharSequence name) {
+             return Names.createLocalName(null, null, name);
+         }
+ 
+         /**
+          * Creates a range for the given minimum and maximum values. We use the static factory methods instead
+          * than the {@link NumberRange} constructor for sharing existing range instances. This is also a way
+          * to ensure that the number type is one of the primitive wrappers.
+          *
+          * <p>This method is invoked for qualitative categories only. For that reason, it accepts NaN values.</p>
+          */
+         private static NumberRange<?> range(final Class<?> type, Number minimum, Number maximum) {
+             switch (Numbers.getEnumConstant(type)) {
+                 case Numbers.BYTE:    return NumberRange.create(minimum.byteValue(),  true, maximum.byteValue(),   true);
+                 case Numbers.SHORT:   return NumberRange.create(minimum.shortValue(), true, maximum.shortValue(),  true);
+                 case Numbers.INTEGER: return NumberRange.create(minimum.intValue(),   true, maximum.intValue(),    true);
+                 case Numbers.LONG:    return NumberRange.create(minimum.longValue(),  true, maximum.longValue(),   true);
+                 case Numbers.FLOAT: {
+                     final float min = minimum.floatValue();
+                     final float max = maximum.floatValue();
+                     if (!Float.isNaN(min) || !Float.isNaN(max)) {       // Let 'create' throws an exception if only one value is NaN.
+                         return NumberRange.create(min, true, max, true);
+                     }
+                     if (minimum.getClass() != Float.class) minimum = min;
+                     if (maximum.getClass() != Float.class) maximum = max;
+                     break;
+                 }
+                 default: {
+                     final double min = minimum.doubleValue();
+                     final double max = maximum.doubleValue();
+                     if (!Double.isNaN(min) || !Double.isNaN(max)) {     // Let 'create' throws an exception if only one value is NaN.
+                         return NumberRange.create(min, true, max, true);
+                     }
+                     if (minimum.getClass() != Double.class) minimum = min;
+                     if (maximum.getClass() != Double.class) maximum = max;
+                     break;
+                 }
+             }
+             @SuppressWarnings({"unchecked", "rawtypes"})
+             final NumberRange<?> samples = new NumberRange(type, minimum, true, maximum, true);
+             return samples;
+         }
+ 
+         /**
+          * Adds a qualitative category and marks that category as the background value.
+          * This is the value to be returned by {@link SampleDimension#getBackground()}.
+          * If this method is invoked more than once, then the last specified value prevails
+          * (previous values become ordinary qualitative categories).
+          *
+          * @param  name    the category name as a {@link String} or {@link InternationalString} object,
+          *                 or {@code null} for a default "fill value" name.
+          * @param  sample  the background value.
+          * @return {@code this}, for method call chaining.
+          */
+         public Builder setBackground(CharSequence name, Number sample) {
+             ArgumentChecks.ensureNonNull("sample", sample);
+             if (name == null) {
+                 name = Vocabulary.formatInternational(Vocabulary.Keys.FillValue);
+             }
+             final NumberRange<?> samples = range(sample.getClass(), sample, sample);
+             background = samples.getMinValue();
+             toNaN.background = background.doubleValue();
+             categories.add(new Category(name, samples, null, null, toNaN));
+             return this;
+         }
+ 
+         /**
+          * Adds a qualitative category for samples of the given boolean value.
+          * The {@code true} value is represented by 1 and the {@code false} value is represented by 0.
+          *
+          * <div class="note"><b>Implementation note:</b>
+          * this convenience method delegates to {@link #addQualitative(CharSequence, NumberRange)}.</div>
+          *
+          * @param  name    the category name as a {@link String} or {@link InternationalString} object,
+          *                 or {@code null} for a default "no data" name.
+          * @param  sample  the sample value as a boolean.
+          * @return {@code this}, for method call chaining.
+          */
+         public Builder addQualitative(final CharSequence name, final boolean sample) {
+             final byte value = sample ? (byte) 1 : 0;
+             return addQualitative(name, NumberRange.create(value, true, value, true));
+         }
+ 
+         /**
+          * Adds a qualitative category for samples of the given tiny (8 bits) integer value.
+          * The argument is treated as a signed integer ({@value Byte#MIN_VALUE} to {@value Byte#MAX_VALUE}).
+          *
+          * <div class="note"><b>Implementation note:</b>
+          * this convenience method delegates to {@link #addQualitative(CharSequence, NumberRange)}.</div>
+          *
+          * @param  name    the category name as a {@link String} or {@link InternationalString} object,
+          *                 or {@code null} for a default "no data" name.
+          * @param  sample  the sample value as an integer.
+          * @return {@code this}, for method call chaining.
+          */
+         public Builder addQualitative(final CharSequence name, final byte sample) {
+             return addQualitative(name, NumberRange.create(sample, true, sample, true));
+         }
+ 
+         /**
+          * Adds a qualitative category for samples of the given short (16 bits) integer value.
+          * The argument is treated as a signed integer ({@value Short#MIN_VALUE} to {@value Short#MAX_VALUE}).
+          *
+          * <div class="note"><b>Implementation note:</b>
+          * this convenience method delegates to {@link #addQualitative(CharSequence, NumberRange)}.</div>
+          *
+          * @param  name    the category name as a {@link String} or {@link InternationalString} object,
+          *                 or {@code null} for a default "no data" name.
+          * @param  sample  the sample value as an integer.
+          * @return {@code this}, for method call chaining.
+          */
+         public Builder addQualitative(final CharSequence name, final short sample) {
+             return addQualitative(name, NumberRange.create(sample, true, sample, true));
+         }
+ 
+         /**
+          * Adds a qualitative category for samples of the given integer value.
+          * The argument is treated as a signed integer ({@value Integer#MIN_VALUE} to {@value Integer#MAX_VALUE}).
+          *
+          * <div class="note"><b>Implementation note:</b>
+          * this convenience method delegates to {@link #addQualitative(CharSequence, NumberRange)}.</div>
+          *
+          * @param  name    the category name as a {@link String} or {@link InternationalString} object,
+          *                 or {@code null} for a default "no data" name.
+          * @param  sample  the sample value as an integer.
+          * @return {@code this}, for method call chaining.
+          */
+         public Builder addQualitative(final CharSequence name, final int sample) {
+             return addQualitative(name, NumberRange.create(sample, true, sample, true));
+         }
+ 
+         /**
+          * Adds a qualitative category for samples of the given floating-point value.
+          *
+          * <div class="note"><b>Implementation note:</b>
+          * this convenience method delegates to {@link #addQualitative(CharSequence, NumberRange)}.</div>
+          *
+          * @param  name    the category name as a {@link String} or {@link InternationalString} object,
+          *                 or {@code null} for a default "no data" name.
+          * @param  sample  the sample value as a real number.
+          * @return {@code this}, for method call chaining.
+          */
+         public Builder addQualitative(final CharSequence name, final float sample) {
+             return addQualitative(name, NumberRange.create(sample, true, sample, true));
+         }
+ 
+         /**
+          * Adds a qualitative category for samples of the given double precision floating-point value.
+          *
+          * <div class="note"><b>Implementation note:</b>
+          * this convenience method delegates to {@link #addQualitative(CharSequence, NumberRange)}.</div>
+          *
+          * @param  name    the category name as a {@link String} or {@link InternationalString} object,
+          *                 or {@code null} for a default "no data" name.
+          * @param  sample  the sample value as a real number.
+          * @return {@code this}, for method call chaining.
+          */
+         public Builder addQualitative(final CharSequence name, final double sample) {
+             return addQualitative(name, NumberRange.create(sample, true, sample, true));
+         }
+ 
+         /**
+          * Adds a qualitative category for samples in the given range of values.
+          *
+          * <div class="note"><b>Implementation note:</b>
+          * this convenience method delegates to {@link #addQualitative(CharSequence, NumberRange)}.</div>
+          *
+          * @param  name     the category name as a {@link String} or {@link InternationalString} object,
+          *                  or {@code null} for a default "no data" name.
+          * @param  minimum  the minimum sample value, inclusive.
+          * @param  maximum  the maximum sample value, inclusive.
+          * @return {@code this}, for method call chaining.
+          * @throws IllegalArgumentException if the range is empty.
+          */
+         public Builder addQualitative(final CharSequence name, final Number minimum, final Number maximum) {
+             return addQualitative(name, range(Numbers.widestClass(minimum, maximum), minimum, maximum));
+         }
+ 
+         /**
+          * Adds a qualitative category for all samples in the specified range of values.
+          * This is the most generic method for adding a qualitative category.
+          * All other {@code addQualitative(name, …)} methods are convenience methods delegating their work to this method.
+          *
+          * @param  name     the category name as a {@link String} or {@link InternationalString} object,
+          *                  or {@code null} for a default "no data" name.
+          * @param  samples  the minimum and maximum sample values in the category.
+          * @return {@code this}, for method call chaining.
+          * @throws IllegalArgumentException if the given range is empty.
+          */
+         public Builder addQualitative(CharSequence name, final NumberRange<?> samples) {
+             if (name == null) {
+                 name = Vocabulary.formatInternational(Vocabulary.Keys.Nodata);
+             }
+             categories.add(new Category(name, samples, null, null, toNaN));
+             return this;
+         }
+ 
+         /**
+          * Constructs a quantitative category mapping samples to real values in the specified range.
+          * Sample values in the {@code samples} range will be mapped to real values in the {@code converted} range
+          * through a linear equation of the form:
+          *
+          * <blockquote><var>measure</var> = <var>sample</var> × <var>scale</var> + <var>offset</var></blockquote>
+          *
+          * where <var>scale</var> and <var>offset</var> coefficients are computed from the ranges supplied in arguments.
+          * The units of measurement will be taken from the {@code converted} range if it is an instance of {@link MeasurementRange}.
+          *
+          * <p><b>Warning:</b> this method is provided for convenience when the scale and offset factors are not explicitly specified.
+          * If those factor are available, then the other {@code addQuantitative(name, samples, …)} methods are more reliable.</p>
+          *
+          * <div class="note"><b>Implementation note:</b>
+          * this convenience method delegates to {@link #addQuantitative(CharSequence, NumberRange, MathTransform1D, Unit)}.</div>
+          *
+          * @param  name        the category name as a {@link String} or {@link InternationalString} object.
+          * @param  samples     the minimum and maximum sample values in the category. Element class is usually
+          *                     {@link Integer}, but {@link Float} and {@link Double} values are accepted as well.
+          * @param  converted   the range of real values for this category, as an instance of {@link MeasurementRange}
+          *                     if those values are associated to an unit of measurement.
+          * @return {@code this}, for method call chaining.
+          * @throws ClassCastException if the range element class is not a {@link Number} subclass.
+          * @throws IllegalArgumentException if the range is invalid.
+          */
+         public Builder addQuantitative(final CharSequence name, final NumberRange<?> samples, final NumberRange<?> converted) {
+             ArgumentChecks.ensureNonNull("samples", samples);
+             ArgumentChecks.ensureNonNull("converted", converted);
+             /*
+              * We need to perform calculation using the same "included versus excluded" characteristics for sample and converted
+              * values. We pickup the characteristics of the range using floating point values because it is easier to adjust the
+              * bounds of the range using integer values (we just add or subtract 1 for integers, while the amount to add to real
+              * numbers is not so clear). If both ranges use floating point values, arbitrarily adjust the converted values.
+              */
+             final boolean isMinIncluded, isMaxIncluded;
+             if (Numbers.isInteger(samples.getElementType())) {
+                 isMinIncluded = converted.isMinIncluded();                         // This is the usual case.
+                 isMaxIncluded = converted.isMaxIncluded();
+             } else {
+                 isMinIncluded = samples.isMinIncluded();                            // Less common case.
+                 isMaxIncluded = samples.isMaxIncluded();
+             }
+             final double minValue  = converted.getMinDouble(isMinIncluded);
+             final double Δvalue    = converted.getMaxDouble(isMaxIncluded) - minValue;
+             final double minSample =   samples.getMinDouble(isMinIncluded);
+             final double Δsample   =   samples.getMaxDouble(isMaxIncluded) - minSample;
+             final double scale     = Δvalue / Δsample;
+             final TransferFunction transferFunction = new TransferFunction();
+             transferFunction.setScale(scale);
+             transferFunction.setOffset(minValue - scale * minSample);               // TODO: use Math.fma with JDK9.
+             return addQuantitative(name, samples, transferFunction.getTransform(),
+                     (converted instanceof MeasurementRange<?>) ? ((MeasurementRange<?>) converted).unit() : null);
+         }
+ 
+         /**
+          * Adds a quantitative category for values ranging from {@code minimum} to {@code maximum} inclusive
+          * in the given units of measurement. The transfer function is set to identity.
+          *
+          * <div class="note"><b>Implementation note:</b>
+          * this convenience method delegates to {@link #addQuantitative(CharSequence, NumberRange, MathTransform1D, Unit)}.</div>
+          *
+          * @param  name     the category name as a {@link String} or {@link InternationalString} object.
+          * @param  minimum  the minimum value (inclusive) in the given units.
+          * @param  maximum  the maximum value (inclusive) in the given units.
+          * @param  units    the units of measurement.
+          * @return {@code this}, for method call chaining.
+          * @throws IllegalArgumentException if a value is NaN or if {@code minimum} is greater than {@code maximum}.
+          */
+         public Builder addQuantitative(CharSequence name, float minimum, float maximum, Unit<?> units) {
+             return addQuantitative(name, MeasurementRange.create(minimum, true, maximum, true, units), Category.identity(), units);
+         }
+ 
+         /**
+          * Adds a quantitative category for values ranging from {@code minimum} to {@code maximum} inclusive
+          * in the given units of measurement. The transfer function is set to identity.
+          *
+          * <div class="note"><b>Implementation note:</b>
+          * this convenience method delegates to {@link #addQuantitative(CharSequence, NumberRange, MathTransform1D, Unit)}.</div>
+          *
+          * @param  name     the category name as a {@link String} or {@link InternationalString} object.
+          * @param  minimum  the minimum value (inclusive) in the given units.
+          * @param  maximum  the maximum value (inclusive) in the given units.
+          * @param  units    the units of measurement.
+          * @return {@code this}, for method call chaining.
+          * @throws IllegalArgumentException if a value is NaN or if {@code minimum} is greater than {@code maximum}.
+          */
+         public Builder addQuantitative(CharSequence name, double minimum, double maximum, Unit<?> units) {
+             return addQuantitative(name, MeasurementRange.create(minimum, true, maximum, true, units), Category.identity(), units);
+         }
+ 
+         /**
+          * Adds a quantitative category for sample values ranging from {@code lower} inclusive to {@code upper} exclusive.
+          * Sample values are converted into real values using the following linear equation:
+          *
+          * <blockquote><var>measure</var> = <var>sample</var> × <var>scale</var> + <var>offset</var></blockquote>
+          *
+          * Results of above conversion are measurements in the units specified by the {@code units} argument.
+          *
+          * <div class="note"><b>Implementation note:</b>
+          * this convenience method delegates to {@link #addQuantitative(CharSequence, NumberRange, MathTransform1D, Unit)}.</div>
+          *
+          * @param  name    the category name as a {@link String} or {@link InternationalString} object.
+          * @param  lower   the lower sample value, inclusive.
+          * @param  upper   the upper sample value, exclusive.
+          * @param  scale   the scale value which is multiplied to sample values for the category. Must be different than zero.
+          * @param  offset  the offset value to add to sample values for this category.
+          * @param  units   the units of measurement of values after conversion by the scale factor and offset.
+          * @return {@code this}, for method call chaining.
+          * @throws IllegalArgumentException if {@code lower} is not smaller than {@code upper},
+          *         or if {@code scale} or {@code offset} are not real numbers, or if {@code scale} is zero.
+          */
+         public Builder addQuantitative(CharSequence name, int lower, int upper, double scale, double offset, Unit<?> units) {
+             final TransferFunction transferFunction = new TransferFunction();
+             transferFunction.setScale(scale);
+             transferFunction.setOffset(offset);
+             return addQuantitative(name, NumberRange.create(lower, true, upper, false), transferFunction.getTransform(), units);
+         }
+ 
+         /**
+          * Constructs a quantitative category for all samples in the specified range of values.
+          * Sample values (usually integers) will be converted into real values
+          * (usually floating-point numbers) through the {@code toUnits} transform.
+          * Results of that conversion are measurements in the units specified by the {@code units} argument.
+          *
+          * <p>This is the most generic method for adding a quantitative category.
+          * All other {@code addQuantitative(name, …)} methods are convenience methods delegating their work to this method.</p>
+          *
+          * @param  name     the category name as a {@link String} or {@link InternationalString} object.
+          * @param  samples  the minimum and maximum sample values in the category. Element class is usually
+          *                  {@link Integer}, but {@link Float} and {@link Double} types are accepted as well.
+          * @param  toUnits  the transfer function from sample values to real values in the specified units.
+          * @param  units    the units of measurement of values after conversion by the transfer function.
+          * @return {@code this}, for method call chaining.
+          * @throws ClassCastException if the range element class is not a {@link Number} subclass.
+          * @throws IllegalArgumentException if the range is invalid.
+          *
+          * @see TransferFunction
+          */
+         public Builder addQuantitative(CharSequence name, NumberRange<?> samples, MathTransform1D toUnits, Unit<?> units) {
+             ArgumentChecks.ensureNonNull("toUnits", toUnits);
+             categories.add(new Category(name, samples, toUnits, units, toNaN));
+             return this;
+         }
+ 
+         /**
+          * Returns {@code true} if the given range intersects the range of a previously added category.
+          * This method can be invoked before to add a new category for checking if it would cause a range collision.
+          *
+          * @param  minimum  minimal value of the range to test, inclusive.
+          * @param  maximum  maximal value of the range to test, inclusive.
+          * @return whether the given range intersects at least one previously added range.
+          */
+         public boolean rangeCollides(final double minimum, final double maximum) {
+             for (final Category category : categories) {
+                 if (maximum >= category.minimum && minimum <= category.maximum) {
+                     return true;
+                 }
+             }
+             return false;
+         }
+ 
+         /**
+          * Creates a new sample with the properties defined to this builder.
+          *
+          * @return the sample dimension.
+          */
+         public SampleDimension build() {
+             GenericName name = dimensionName;
+ defName:    if (name == null) {
+                 for (final Category category : categories) {
+                     if (category.isQuantitative()) {
+                         name = createLocalName(category.name);
+                         break defName;
+                     }
+                 }
+                 name = createLocalName(Vocabulary.formatInternational(Vocabulary.Keys.Untitled));
+             }
+             return new SampleDimension(name, background, categories);
+         }
+ 
+         /**
+          * Reset this builder to the same state than after construction.
+          * The sample dimension name, background values and all categories are discarded.
+          * This method can be invoked when the same builder is reused for creating more than one sample dimension.
+          */
+         public void clear() {
+             dimensionName = null;
+             background    = null;
+             categories.clear();
+             toNaN.clear();
+         }
+     }
+ }
diff --cc core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
index 0000000,aebef90..fc2c60a
mode 000000,100644..100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
@@@ -1,0 -1,189 +1,185 @@@
+ /*
+  * 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.coverage.grid;
+ 
+ import java.util.List;
+ import java.util.Collection;
+ import java.util.Locale;
+ import java.awt.image.RenderedImage;
+ import org.opengis.geometry.DirectPosition;
 -import org.opengis.coverage.CannotEvaluateException;
 -import org.opengis.coverage.PointOutsideCoverageException;
+ import org.opengis.referencing.crs.CoordinateReferenceSystem;
+ import org.apache.sis.internal.util.UnmodifiableArrayList;
+ import org.apache.sis.coverage.SampleDimension;
+ import org.apache.sis.util.collection.DefaultTreeTable;
+ import org.apache.sis.util.collection.TableColumn;
+ import org.apache.sis.util.collection.TreeTable;
+ import org.apache.sis.util.resources.Vocabulary;
+ import org.apache.sis.util.ArgumentChecks;
+ import org.apache.sis.util.Classes;
+ import org.apache.sis.util.Debug;
+ 
+ 
+ /**
+  * Base class of coverages with domains defined as a set of grid points.
+  * The essential property of coverage is to be able to generate a value for any point within its domain.
+  * Since a grid coverage is represented by a grid of values, the value returned by the coverage for a point
+  * is that of the grid value whose location is nearest the point.
+  *
+  * @author  Martin Desruisseaux (IRD, Geomatys)
+  * @version 1.0
+  * @since   1.0
+  * @module
+  */
+ public abstract class GridCoverage {
+     /**
+      * The grid extent, coordinate reference system (CRS) and conversion from cell indices to CRS.
+      */
+     private final GridGeometry gridGeometry;
+ 
+     /**
+      * List of sample dimension (band) information for the grid coverage. Information include such things
+      * as description, the no data values, minimum and maximum values, <i>etc</i>. A coverage must have
+      * at least one sample dimension. The content of this array shall never be modified.
+      */
+     private final SampleDimension[] sampleDimensions;
+ 
+     /**
+      * Constructs a grid coverage using the specified grid geometry and sample dimensions.
+      *
+      * @param grid   the grid extent, CRS and conversion from cell indices to CRS.
+      * @param bands  sample dimensions for each image band.
+      */
+     protected GridCoverage(final GridGeometry grid, final Collection<? extends SampleDimension> bands) {
+         ArgumentChecks.ensureNonNull("grid",  grid);
+         ArgumentChecks.ensureNonNull("bands", bands);
+         gridGeometry = grid;
+         sampleDimensions = bands.toArray(new SampleDimension[bands.size()]);
+         for (int i=0; i<sampleDimensions.length; i++) {
+             ArgumentChecks.ensureNonNullElement("bands", i, sampleDimensions[i]);
+         }
+     }
+ 
+     /**
+      * Returns the coordinate reference system to which the values in grid domain are referenced.
+      * This is the CRS used when accessing a coverage with the {@code evaluate(…)} methods.
+      * This coordinate reference system is usually different than the coordinate system of the grid.
+      * It is the target coordinate reference system of the {@link GridGeometry#getGridToCRS gridToCRS}
+      * math transform.
+      *
+      * <p>The default implementation delegates to {@link GridGeometry#getCoordinateReferenceSystem()}.</p>
+      *
+      * @return the CRS used when accessing a coverage with the {@code evaluate(…)} methods.
+      * @throws IncompleteGridGeometryException if the grid geometry has no CRS.
+      */
+     public CoordinateReferenceSystem getCoordinateReferenceSystem() {
+         return gridGeometry.getCoordinateReferenceSystem();
+     }
+ 
+     /**
+      * Returns information about the <cite>domain</cite> of this grid coverage.
+      * Information includes the grid extent, CRS and conversion from cell indices to CRS.
+      * {@code GridGeometry} can also provide derived information like bounding box and resolution.
+      *
+      * @return grid extent, CRS and conversion from cell indices to CRS.
+      */
+     public GridGeometry getGridGeometry() {
+         return gridGeometry;
+     }
+ 
+     /**
+      * Returns information about the <cite>range</cite> of this grid coverage.
+      * Information include names, sample value ranges, fill values and transfer functions for all bands in this grid coverage.
+      *
+      * @return names, value ranges, fill values and transfer functions for all bands in this grid coverage.
+      */
+     public List<SampleDimension> getSampleDimensions() {
+         return UnmodifiableArrayList.wrap(sampleDimensions);
+     }
+ 
+     /**
+      * Returns a two-dimensional slice of grid data as a rendered image. The given {@code slicePoint} argument specifies
+      * the coordinates of the slice in all dimensions that are not in the two-dimensional image. For example if this grid
+      * coverage has (<var>x</var>, <var>y</var>, <var>z</var>, <var>t</var>) dimensions and we want to render an image
+      * of data in the (<var>x</var>, <var>y</var>) dimensions, then the given {@code slicePoint} shall contain the
+      * (<var>z</var>, <var>t</var>) coordinates of the desired slice. The two coordinates of the data to be shown
+      * (<var>x</var> and <var>y</var> in our example) shall be excluded from the slice point in one of the following ways:
+      *
+      * <ul>
+      *   <li>The {@code slicePoint} has a CRS with two dimensions less than this grid coverage CRS.</li>
+      *   <li>The {@code slicePoint} has the same CRS than this grid coverage, but the two coordinates to
+      *       exclude are set to {@link Double#NaN}.</li>
+      * </ul>
+      *
+      * If the {@code slicePoint} CRS is different than this grid coverage CRS (except for the number of dimensions),
+      * a coordinate transformation will be applied. If the {@code slicePoint} CRS is {@code null}, it is assumed the
+      * same than this grid coverage CRS. If this grid coverage is two-dimensional or can render only one image for
+      * other reason, then the {@code slicePoint} can be null.
+      *
+      * <p>Implementations should return a view as much as possible, without copying sample values.</p>
+      *
+      * @param  slicePoint  coordinates of the slice in all dimensions other than the two dimensions to be shown on the image.
+      *         May be {@code null} if this coverage can render only one image, for example because its CRS is two-dimensional.
+      * @return the grid slice as a rendered image.
 -     * @throws PointOutsideCoverageException if the given slice point is illegal.
 -     * @throws CannotEvaluateException if this method can not produce the render image for another reason.
+      */
 -    public abstract RenderedImage render(DirectPosition slicePoint) throws CannotEvaluateException;
++    public abstract RenderedImage render(DirectPosition slicePoint);
+ 
+     /**
+      * Returns a string representation of this grid coverage for debugging purpose.
+      * The returned string is implementation dependent and may change in any future version.
+      * Current implementation is equivalent to the following, where {@code EXTENT}, <i>etc.</i> are
+      * constants defined in {@link GridGeometry} class:
+      *
+      * {@preformat java
+      *   return toTree(Locale.getDefault(), EXTENT | ENVELOPE | CRS | GRID_TO_CRS | RESOLUTION).toString();
+      * }
+      *
+      * @return a string representation of this grid coverage for debugging purpose.
+      */
+     @Override
+     public String toString() {
+         return toTree(Locale.getDefault(), GridGeometry.EXTENT | GridGeometry.ENVELOPE
+                 | GridGeometry.CRS | GridGeometry.GRID_TO_CRS | GridGeometry.RESOLUTION).toString();
+     }
+ 
+     /**
+      * Returns a tree representation of some elements of this grid coverage.
+      * The tree representation is for debugging purpose only and may change
+      * in any future SIS version.
+      *
+      * @param  locale   the locale to use for textual labels.
+      * @param  bitmask  combination of {@link GridGeometry} flags.
+      * @return a tree representation of the specified elements.
+      *
+      * @see GridGeometry#toTree(Locale, int)
+      */
+     @Debug
+     public TreeTable toTree(final Locale locale, final int bitmask) {
+         ArgumentChecks.ensureNonNull("locale", locale);
+         final Vocabulary vocabulary = Vocabulary.getResources(locale);
+         final TableColumn<CharSequence> column = TableColumn.VALUE_AS_TEXT;
+         final TreeTable tree = new DefaultTreeTable(column);
+         final TreeTable.Node root = tree.getRoot();
+         root.setValue(column, Classes.getShortClassName(this));
+         TreeTable.Node branch = root.newChild();
+         branch.setValue(column, vocabulary.getString(Vocabulary.Keys.CoverageDomain));
+         gridGeometry.formatTo(locale, vocabulary, bitmask, branch);
+         branch = root.newChild();
+         branch.setValue(column, vocabulary.getString(Vocabulary.Keys.SampleDimensions));
+         branch.newChild().setValue(column, SampleDimension.toString(locale, sampleDimensions));
+         return tree;
+     }
+ }
diff --cc core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
index bdb1491,0e17166..1a4e390
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
@@@ -356,17 -478,35 +475,17 @@@ public class GridExtent implements Seri
       * @param  extent  the grid envelope to copy.
       * @throws IllegalArgumentException if a coordinate value in the low part is
       *         greater than the corresponding coordinate value in the high part.
 -     *
 -     * @see #castOrCopy(GridEnvelope)
       */
 -    protected GridExtent(final GridEnvelope extent) {
 +    protected GridExtent(final GridExtent extent) {
          ArgumentChecks.ensureNonNull("extent", extent);
          final int dimension = extent.getDimension();
-         ordinates = allocate(dimension);
+         coordinates = allocate(dimension);
          for (int i=0; i<dimension; i++) {
-             ordinates[i] = extent.getLow(i);
-             ordinates[i + dimension] = extent.getHigh(i);
+             coordinates[i] = extent.getLow(i);
+             coordinates[i + dimension] = extent.getHigh(i);
          }
-         checkCoherence(ordinates);
+         checkCoherence(coordinates);
 -        types = (extent instanceof GridExtent) ? ((GridExtent) extent).types : null;
 -    }
 -
 -    /**
 -     * Returns the given grid envelope as a {@code GridExtent} implementation.
 -     * If the given extent is already a {@code GridExtent} instance or is null, then it is returned as-is.
 -     * Otherwise a new extent is created using the {@linkplain #GridExtent(GridEnvelope) copy constructor}.
 -     *
 -     * @param  extent  the grid envelope to cast or copy, or {@code null}.
 -     * @return the grid envelope as a {@code GridExtent}, or {@code null} if the given extent was null.
 -     */
 -    public static GridExtent castOrCopy(final GridEnvelope extent) {
 -        if (extent == null || extent instanceof GridExtent) {
 -            return (GridExtent) extent;
 -        } else {
 -            return new GridExtent(extent);
 -        }
 +        types = extent.types;
      }
  
      /**
diff --cc core/sis-raster/src/test/java/org/apache/sis/image/DefaultIteratorTest.java
index 15f7c84,8c4832f..da83542
--- a/core/sis-raster/src/test/java/org/apache/sis/image/DefaultIteratorTest.java
+++ b/core/sis-raster/src/test/java/org/apache/sis/image/DefaultIteratorTest.java
@@@ -25,6 -25,8 +25,7 @@@ import java.awt.image.Raster
  import java.awt.image.WritableRaster;
  import java.awt.image.WritableRenderedImage;
  import java.nio.FloatBuffer;
 -import org.opengis.coverage.grid.SequenceType;
+ import org.apache.sis.util.ArraysExt;
  import org.apache.sis.test.DependsOnMethod;
  import org.apache.sis.test.TestCase;
  import org.junit.After;
diff --cc core/sis-referencing-by-identifiers/src/main/java/org/apache/sis/referencing/gazetteer/ModifiableLocationType.java
index 5c2754b,deb5dd9..8bf8273
--- a/core/sis-referencing-by-identifiers/src/main/java/org/apache/sis/referencing/gazetteer/ModifiableLocationType.java
+++ b/core/sis-referencing-by-identifiers/src/main/java/org/apache/sis/referencing/gazetteer/ModifiableLocationType.java
@@@ -350,14 -351,9 +350,14 @@@ public class ModifiableLocationType ext
  
      /**
       * Returns the name of organization or class of organization able to create and destroy location instances.
-      * If no organization has been explicitely set, then this method inherits the value from
+      * If no organization has been explicitly set, then this method inherits the value from
       * the parents providing that all parents specify the same organization.
       *
 +     * <div class="warning"><b>Upcoming API change — generalization</b><br>
 +     * in a future SIS version, the type of returned element may be generalized to the
 +     * {@code org.opengis.metadata.citation.Party} interface. This change is pending
 +     * GeoAPI revision for upgrade from ISO 19115:2003 to ISO 19115:2014.</div>
 +     *
       * @return organization or class of organization able to create and destroy location instances,
       *         or {@code null} if no value has been defined or can be inherited.
       *
diff --cc core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ReferencingFactoryContainer.java
index 0000000,10144fc..0e4b1ae
mode 000000,100644..100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ReferencingFactoryContainer.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ReferencingFactoryContainer.java
@@@ -1,0 -1,213 +1,215 @@@
+ /*
+  * 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;
+ 
+ import org.opengis.util.FactoryException;
+ import org.opengis.referencing.cs.CSFactory;
+ import org.opengis.referencing.cs.CSAuthorityFactory;
+ import org.opengis.referencing.crs.CRSFactory;
+ import org.opengis.referencing.crs.CRSAuthorityFactory;
+ import org.opengis.referencing.datum.DatumFactory;
+ import org.opengis.referencing.datum.DatumAuthorityFactory;
+ import org.opengis.referencing.operation.MathTransformFactory;
 -import org.opengis.referencing.operation.CoordinateOperationFactory;
+ import org.opengis.referencing.operation.CoordinateOperationAuthorityFactory;
+ import org.apache.sis.referencing.factory.NoSuchAuthorityFactoryException;
+ import org.apache.sis.internal.system.DefaultFactories;
+ import org.apache.sis.internal.util.Constants;
+ import org.apache.sis.referencing.CRS;
+ 
++// Branch-dependent imports
++import org.apache.sis.referencing.operation.DefaultCoordinateOperationFactory;
++
+ 
+ /**
+  * A container of factories frequently used together.
+  * This class may be temporary until we choose a dependency injection framework
+  * See <a href="https://issues.apache.org/jira/browse/SIS-102">SIS-102</a>.
+  *
+  * <p>This class is not thread safe. Synchronization, if needed, is caller's responsibility.</p>
+  *
+  * @author  Martin Desruisseaux (IRD, Geomatys)
+  * @version 1.0
+  *
+  * @see <a href="https://issues.apache.org/jira/browse/SIS-102">SIS-102</a>
+  *
+  * @since 1.0
+  * @module
+  */
+ public class ReferencingFactoryContainer {
+     /**
+      * The factory for creating coordinate reference systems from authority codes.
+      * If null, then a default factory will be created only when first needed.
+      */
+     private CRSAuthorityFactory crsAuthorityFactory;
+ 
+     /**
+      * The {@linkplain org.opengis.referencing.datum.Datum datum} factory.
+      * If null, then a default factory will be created only when first needed.
+      */
+     private DatumFactory datumFactory;
+ 
+     /**
+      * The {@linkplain org.opengis.referencing.cs.CoordinateSystem coordinate system} factory.
+      * If null, then a default factory will be created only when first needed.
+      */
+     private CSFactory csFactory;
+ 
+     /**
+      * The {@linkplain org.opengis.referencing.crs.CoordinateReferenceSystem coordinate reference system} factory.
+      * If null, then a default factory will be created only when first needed.
+      */
+     private CRSFactory crsFactory;
+ 
+     /**
+      * Factory for fetching operation methods and creating defining conversions.
+      * This is needed only for user-defined projected coordinate reference system.
+      */
 -    private CoordinateOperationFactory operationFactory;
++    private DefaultCoordinateOperationFactory operationFactory;
+ 
+     /**
+      * The {@linkplain org.opengis.referencing.operation.MathTransform math transform} factory.
+      * If null, then a default factory will be created only when first needed.
+      */
+     private MathTransformFactory mtFactory;
+ 
+     /**
+      * Creates a new instance for the default factories.
+      */
+     public ReferencingFactoryContainer() {
+     }
+ 
+     /**
+      * Returns the factory for creating coordinate reference systems from authority codes.
+      * Currently only EPSG codes are supported.
+      *
+      * @return the Coordinate Reference System authority factory (never {@code null}).
+      * @throws FactoryException if the authority factory can not be obtained.
+      */
+     public final CRSAuthorityFactory getCRSAuthorityFactory() throws FactoryException {
+         if (crsAuthorityFactory == null) {
+             crsAuthorityFactory = CRS.getAuthorityFactory(Constants.EPSG);
+         }
+         return crsAuthorityFactory;
+     }
+ 
+     /**
+      * Returns the factory for creating coordinate systems from authority codes.
+      * Currently only EPSG codes are supported.
+      *
+      * @return the Coordinate System authority factory (never {@code null}).
+      * @throws FactoryException if the authority factory can not be obtained.
+      */
+     public final CSAuthorityFactory getCSAuthorityFactory() throws FactoryException {
+         final CRSAuthorityFactory factory = getCRSAuthorityFactory();
+         if (factory instanceof CSAuthorityFactory) {                    // This is the case for SIS implementation.
+             return (CSAuthorityFactory) factory;
+         }
+         throw new NoSuchAuthorityFactoryException(null, Constants.EPSG);
+     }
+ 
+     /**
+      * Returns the factory for creating datum from authority codes.
+      * Currently only EPSG codes are supported.
+      *
+      * @return the Datum authority factory (never {@code null}).
+      * @throws FactoryException if the authority factory can not be obtained.
+      */
+     public final DatumAuthorityFactory getDatumAuthorityFactory() throws FactoryException {
+         final CRSAuthorityFactory factory = getCRSAuthorityFactory();
+         if (factory instanceof DatumAuthorityFactory) {                 // This is the case for SIS implementation.
+             return (DatumAuthorityFactory) factory;
+         }
+         throw new NoSuchAuthorityFactoryException(null, Constants.EPSG);
+     }
+ 
+     /**
+      * Returns the factory for creating coordinate operations from authority codes.
+      * Currently only EPSG codes are supported.
+      *
+      * @return the Coordinate Operation authority factory (never {@code null}).
+      * @throws FactoryException if the authority factory can not be obtained.
+      */
+     public final CoordinateOperationAuthorityFactory getCoordinateOperationAuthorityFactory() throws FactoryException {
+         final CRSAuthorityFactory factory = getCRSAuthorityFactory();
+         if (factory instanceof CoordinateOperationAuthorityFactory) {       // This is the case for SIS implementation.
+             return (CoordinateOperationAuthorityFactory) factory;
+         }
+         throw new NoSuchAuthorityFactoryException(null, Constants.EPSG);
+     }
+ 
+     /**
+      * Returns the factory for creating coordinate reference systems.
+      *
+      * @return the Coordinate Reference System factory (never {@code null}).
+      */
+     public final CRSFactory getCRSFactory() {
+         if (crsFactory == null) {
+             crsFactory = DefaultFactories.forBuildin(CRSFactory.class);
+         }
+         return crsFactory;
+     }
+ 
+     /**
+      * Returns the factory for creating coordinate systems and their axes.
+      *
+      * @return the Coordinate System factory (never {@code null}).
+      */
+     public final CSFactory getCSFactory() {
+         if (csFactory == null) {
+             csFactory = DefaultFactories.forBuildin(CSFactory.class);
+         }
+         return csFactory;
+     }
+ 
+     /**
+      * Returns the factory for creating datum, prime meridians and ellipsoids.
+      *
+      * @return the Datum factory (never {@code null}).
+      */
+     public final DatumFactory getDatumFactory() {
+         if (datumFactory == null) {
+             datumFactory = DefaultFactories.forBuildin(DatumFactory.class);
+         }
+         return datumFactory;
+     }
+ 
+     /**
+      * Returns the factory for fetching operation methods and creating defining conversions.
+      * This is needed only for user-defined projected coordinate reference system.
+      * The factory is fetched when first needed.
+      *
+      * @return the Coordinate Operation factory (never {@code null}).
+      */
 -    public final CoordinateOperationFactory getCoordinateOperationFactory() {
++    public final DefaultCoordinateOperationFactory getCoordinateOperationFactory() {
+         if (operationFactory == null) {
+             operationFactory = CoordinateOperations.factory();
+         }
+         return operationFactory;
+     }
+ 
+     /**
+      * Returns the factory for creating parameterized transforms.
+      *
+      * @return the Math Transform factory (never {@code null}).
+      */
+     public final MathTransformFactory getMathTransformFactory() {
+         if (mtFactory == null) {
+             mtFactory = DefaultFactories.forBuildin(MathTransformFactory.class);
+         }
+         return mtFactory;
+     }
+ }
diff --cc core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/matrix/Matrix4Test.java
index 0880923,3632436..a8c6208
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/matrix/Matrix4Test.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/matrix/Matrix4Test.java
@@@ -19,7 -19,8 +19,8 @@@ package org.apache.sis.referencing.oper
  import org.apache.sis.test.DependsOn;
  import org.junit.Test;
  
- import static org.junit.Assert.*;
+ import static java.lang.Double.NaN;
 -import static org.opengis.test.Assert.*;
++import static org.apache.sis.test.Assert.*;
  import static org.apache.sis.referencing.operation.matrix.Matrix4.SIZE;
  
  
diff --cc core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/AbridgedMolodenskyTransformTest.java
index 9d8a33c,abebea9..9b2cb66
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/AbridgedMolodenskyTransformTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/AbridgedMolodenskyTransformTest.java
@@@ -114,10 -118,11 +114,10 @@@ public final strictfp class AbridgedMol
          verifyInDomain(CoordinateDomain.GEOGRAPHIC, -1941624852762631518L);
          /*
           * Calculation of "expected" transform derivative by finite difference
-          * seems too approximative for the default accuracy. (Actually, we are
-          * not completely sure that there is no bug in derivative formula...)
+          * does not seem accurate enough for the default accuracy. (Actually,
+          * we are not completely sure that there is no bug in derivative formula).
           */
          tolerance *= 10;
 -        isDerivativeSupported = true;
          verifyInDomain(CoordinateDomain.GEOGRAPHIC, 4350796528249596132L);
      }
  
diff --cc core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/MathTransformTestCase.java
index 98e17da,dc2be7b..c94414f
--- 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
@@@ -18,16 -18,21 +18,18 @@@ package org.apache.sis.referencing.oper
  
  import java.util.Random;
  import java.io.IOException;
+ import java.io.UncheckedIOException;
 -import org.opengis.util.Factory;
  import org.opengis.referencing.operation.MathTransform;
  import org.opengis.referencing.operation.MathTransform1D;
  import org.opengis.referencing.operation.MathTransform2D;
  import org.opengis.referencing.operation.TransformException;
  import org.opengis.parameter.ParameterDescriptorGroup;
  import org.opengis.parameter.ParameterValueGroup;
 -import org.opengis.geometry.DirectPosition;
  import org.opengis.metadata.Identifier;
  import org.apache.sis.parameter.Parameterized;
 -import org.apache.sis.measure.Longitude;
  import org.apache.sis.util.Debug;
  import org.apache.sis.util.Classes;
+ import org.apache.sis.util.ArraysExt;
  import org.apache.sis.io.TableAppender;
  import org.apache.sis.io.wkt.Convention;
  import org.apache.sis.io.wkt.FormattableObject;
diff --cc core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/PassThroughTransformTest.java
index 7a6b5ea,2b6398b..10c6725
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/PassThroughTransformTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/PassThroughTransformTest.java
@@@ -29,10 -32,11 +32,10 @@@ import org.apache.sis.util.ArraysExt
  import org.junit.Test;
  import org.apache.sis.test.TestUtilities;
  import org.apache.sis.test.DependsOn;
--import static org.junit.Assert.*;
++import static org.apache.sis.test.Assert.*;
  
  // Branch-dependent imports
 -import org.opengis.test.CalculationType;
 -import org.opengis.test.ToleranceModifier;
 +// (all imports removed)
  
  
  /**
@@@ -192,23 -221,84 +220,72 @@@ public final strictfp class PassThrough
           * Now process to the transform and compares the results with the expected ones.
           */
          tolerance         = 0;          // Results should be strictly identical because we used the same inputs.
-         final double[] transformedData = new double[expectedData.length];
 -        toleranceModifier = null;
+         final double[] transformedData = new double[StrictMath.max(sourceDim, targetDim) * numPts];
          transform.transform(passthroughData, 0, transformedData, 0, numPts);
-         assertCoordinatesEqual("Direct transform.", passthroughDim,
-                 expectedData, 0, transformedData, 0, numPts, false);
+         assertCoordinatesEqual("PassThroughTransform results do not match the results computed by this test.",
 -                targetDim, expectedData, 0, transformedData, 0, numPts, CalculationType.DIRECT_TRANSFORM);
++                targetDim, expectedData, 0, transformedData, 0, numPts, false);
          /*
           * Test inverse transform.
           */
-         tolerance = 1E-8;
-         Arrays.fill(transformedData, Double.NaN);
-         transform.inverse().transform(expectedData, 0, transformedData, 0, numPts);
-         assertCoordinatesEqual("Inverse transform.", passthroughDim,
-                 passthroughData, 0, transformedData, 0, numPts, false);
+         if (isInverseTransformSupported) {
+             tolerance         = 1E-8;
 -            toleranceModifier = ToleranceModifier.RELATIVE;
+             Arrays.fill(transformedData, Double.NaN);
+             transform.inverse().transform(expectedData, 0, transformedData, 0, numPts);
+             assertCoordinatesEqual("Inverse of PassThroughTransform do not give back the original data.",
 -                    sourceDim, passthroughData, 0, transformedData, 0, numPts, CalculationType.INVERSE_TRANSFORM);
++                    sourceDim, passthroughData, 0, transformedData, 0, numPts, false);
+         }
          /*
           * Verify the consistency between different 'transform(…)' methods.
           */
-         final float[] sourceAsFloat = Numerics.copyAsFloats(passthroughData);
+         final float[] sourceAsFloat = ArraysExt.copyAsFloats(passthroughData);
          final float[] targetAsFloat = verifyConsistency(sourceAsFloat);
          assertEquals("Unexpected length of transformed array.", expectedData.length, targetAsFloat.length);
 -        /*
 -         * We use a relatively high tolerance threshold because result are
 -         * computed using inputs stored as float values.
 -         */
 -        if (transform instanceof LinearTransform) {
 -            tolerance         = 1E-4;
 -            toleranceModifier = ToleranceModifier.RELATIVE;
 -            assertCoordinatesEqual("PassThroughTransform.transform(…) variants produce inconsistent results.",
 -                    sourceDim, expectedData, 0, targetAsFloat, 0, numPts, CalculationType.DIRECT_TRANSFORM);
 -        }
      }
+ 
+     /**
+      * Tests {@link PassThroughTransform#tryConcatenate(boolean, MathTransform, MathTransformFactory)}.
+      * This tests creates a non-linear transform of 6→7 dimensions, then applies a filter keeping only
+      * target dimensions 1, 4 and 6 (corresponding to source dimensions 1 and 5).
+      *
+      * @throws FactoryException if an error occurred while combining the transforms.
+      */
+     @Test
+     public void testTryConcatenate() throws FactoryException {
+         PassThroughTransform ps = PassThroughTransform.newInstance(2, new PseudoTransform(2, 3), 2);
+         MathTransform c = ps.tryConcatenate(false, MathTransforms.linear(Matrices.create(4, 8, new double[] {
+                 0, 1, 0, 0, 0, 0, 0, 0,
+                 0, 0, 0, 0, 1, 0, 0, 0,
+                 0, 0, 0, 0, 0, 0, 1, 0,
+                 0, 0, 0, 0, 0, 0, 0, 1})), null);
+ 
+         final List<MathTransform> steps = MathTransforms.getSteps(c);
+         assertEquals("Number of steps", 3, steps.size());
+         /*
+          * We need to remove source dimensions 0, 2, 3 and 4. We can not remove dimensions 2 and 3 before
+          * pass-through because they are used by the sub-transform. It leaves us dimensions 0 and 4 which
+          * can be removed here.
+          */
+         assertMatrixEquals("Expected removal of dimensions 0 and 4 before pass-through", Matrices.create(5, 7, new double[] {
+                 0, 1, 0, 0, 0, 0, 0,
+                 0, 0, 1, 0, 0, 0, 0,
+                 0, 0, 0, 1, 0, 0, 0,
+                 0, 0, 0, 0, 0, 1, 0,
 -                0, 0, 0, 0, 0, 0, 1}), MathTransforms.getMatrix(steps.get(0)), null);
++                0, 0, 0, 0, 0, 0, 1}), MathTransforms.getMatrix(steps.get(0)), 0);
+         /*
+          * The number of pass-through dimensions have decreased from 2 to 1 on both sides of the sub-transform.
+          */
+         final PassThroughTransform reduced = (PassThroughTransform) steps.get(1);
+         assertEquals("firstAffectedOrdinate", 1, reduced.firstAffectedOrdinate);
+         assertEquals("numTrailingOrdinates",  1, reduced.numTrailingOrdinates);
+         assertSame  ("subTransform", ps.subTransform, reduced.subTransform);
+         /*
+          * We still have to remove source dimensions 2 and 3. Since we removed dimension 0 in previous step,
+          * the indices of dimensions to removed have shifted to 1 and 2.
+          */
+         assertMatrixEquals("Expected removal of dimensions 1 and 2 after pass-through", Matrices.create(4, 6, new double[] {
+                 1, 0, 0, 0, 0, 0,
+                 0, 0, 0, 1, 0, 0,
+                 0, 0, 0, 0, 1, 0,
 -                0, 0, 0, 0, 0, 1}), MathTransforms.getMatrix(steps.get(2)), null);
++                0, 0, 0, 0, 0, 1}), MathTransforms.getMatrix(steps.get(2)), 0);
+     }
  }
diff --cc core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/TransformSeparatorTest.java
index 9b821d6,743cfbe..ce546b6
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/TransformSeparatorTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/TransformSeparatorTest.java
@@@ -30,7 -37,8 +37,8 @@@ import org.apache.sis.test.DependsOn
  import org.apache.sis.test.TestCase;
  import org.junit.Test;
  
+ import static java.lang.Double.NaN;
 -import static org.opengis.test.Assert.*;
 +import static org.apache.sis.test.Assert.*;
  
  
  /**
diff --cc core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/TranslationTransformTest.java
index 0000000,ae4c4aa..00381b5
mode 000000,100644..100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/TranslationTransformTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/TranslationTransformTest.java
@@@ -1,0 -1,114 +1,113 @@@
+ /*
+  * 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.transform;
+ 
+ import org.opengis.referencing.operation.TransformException;
+ import org.apache.sis.referencing.operation.matrix.MatrixSIS;
+ import org.apache.sis.referencing.operation.matrix.Matrices;
+ import org.apache.sis.referencing.operation.matrix.Matrix4;
+ import org.apache.sis.internal.referencing.ExtendedPrecisionMatrix;
+ import org.apache.sis.internal.util.DoubleDouble;
+ 
+ import org.apache.sis.test.DependsOnMethod;
+ import org.apache.sis.test.DependsOn;
 -import org.opengis.test.Assert;
+ import org.junit.Test;
+ 
 -import static org.opengis.test.Assert.*;
++import static org.apache.sis.test.Assert.*;
+ 
+ 
+ /**
+  * Tests the {@link TranslationTransform} class.
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  * @version 1.0
+  * @since   1.0
+  * @module
+  */
+ @DependsOn(AbstractMathTransformTest.class)
+ public final strictfp class TranslationTransformTest extends MathTransformTestCase {
+     /**
+      * Sets the {@link #transform} field to the {@link TranslationTransform} instance to test.
+      *
+      * @param  dimensions  expected number of source and target dimensions.
+      * @param  matrix      the data to use for creating the transform.
+      */
+     private void create(final int dimensions, final MatrixSIS matrix) {
+         final double[] elements = matrix.getElements();
+         final TranslationTransform tr = new TranslationTransform(matrix.getNumRow(), elements);
+         assertEquals("sourceDimensions", dimensions, tr.getSourceDimensions());
+         assertEquals("targetDimensions", dimensions, tr.getTargetDimensions());
 -        Assert.assertMatrixEquals("matrix", matrix, tr.getMatrix(), 0.0);
++        assertMatrixEquals("matrix", matrix, tr.getMatrix(), 0.0);
+         assertArrayEquals("elements", elements, tr.getExtendedElements(), 0.0);
+         transform = tr;
+         validate();
+     }
+ 
+     /**
+      * Tests a transform created from a square matrix with no error terms.
+      *
+      * @throws TransformException should never happen.
+      */
+     @Test
+     public void testConstantDimension() throws TransformException {
+         create(3, new Matrix4(
+                 1, 0, 0, 2,
+                 0, 1, 0, 3,
+                 0, 0, 1, 8,
+                 0, 0, 0, 1));
+ 
+         verifyTransform(new double[] {1,1,1,   6, 0,  2,   2, Double.NaN,  6},
+                         new double[] {3,4,9,   8, 3, 10,   4, Double.NaN, 14});
+     }
+ 
+     /**
+      * Verifies that {@link TranslationTransform} stores the error terms when they exist.
+      */
+     @Test
+     @DependsOnMethod("testConstantDimension")
+     public void testExtendedPrecision() {
+         final Number O = 0;
+         final Number l = 1;
+         final DoubleDouble r = DoubleDouble.createDegreesToRadians();
+         final MatrixSIS matrix = Matrices.create(4, 4, new Number[] {
+             l, O, O, r,
+             O, l, O, r,
+             O, O, l, O,
+             O, O, O, l
+         });
+         final double[] elements = ((ExtendedPrecisionMatrix) matrix).getExtendedElements();
+         assertTrue (r.value > r.error);
+         assertFalse(r.error == 0);          // Paranoiac checks for making sure that next assertion will test something.
+         assertArrayEquals(new double[] {    // Paranoiac check for making sure that getExtendedElements() is not broken.
+                 1, 0, 0, r.value,
+                 0, 1, 0, r.value,
+                 0, 0, 1, 0,
+                 0, 0, 0, 1,
+                 0, 0, 0, r.error,
+                 0, 0, 0, r.error,
+                 0, 0, 0, 0,
+                 0, 0, 0, 0}, elements, 0);
+ 
+         final TranslationTransform tr = new TranslationTransform(4, elements);
+         assertEquals("sourceDimensions", 3, tr.getSourceDimensions());
+         assertEquals("targetDimensions", 3, tr.getTargetDimensions());
 -        Assert.assertMatrixEquals("matrix", matrix, tr.getMatrix(), 0.0);
++        assertMatrixEquals("matrix", matrix, tr.getMatrix(), 0.0);
+         assertArrayEquals("elements", elements, tr.getExtendedElements(), 0.0);
+         transform = tr;
+         validate();
+     }
+ }
diff --cc core/sis-utility/src/main/java/org/apache/sis/measure/MeasurementRange.java
index 595e1a9,59d03c5..eb798b6
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/MeasurementRange.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/MeasurementRange.java
@@@ -227,7 -235,17 +235,10 @@@ public class MeasurementRange<E extend
  
      /**
       * Returns the unit of measurement, or {@code null} if unknown.
+      * In principle the unit should never be null, otherwise a {@link NumberRange} should have been used
+      * instead than {@code MeasurementRange}. Nevertheless this method may return {@code null} if a unit
+      * <em>should</em> exist but for some reason is unavailable.
       *
 -     * <div class="note"><b>Example:</b>
 -     * ISO 19115-1 {@code SampleDimension} specifies that its
 -     * {@linkplain org.opengis.metadata.content.SampleDimension#getUnits() unit} property is mandatory if the
 -     * {@linkplain org.opengis.metadata.content.SampleDimension#getMinValue() minimum value} or
 -     * {@linkplain org.opengis.metadata.content.SampleDimension#getMaxValue() maximum value} are provided.
 -     * Nevertheless it happens sometime that this information is missing in metadata.</div>
 -     *
       * @return the unit of measurement, or {@code null}.
       */
      @Override
diff --cc core/sis-utility/src/main/java/org/apache/sis/util/resources/IndexedResourceBundle.java
index a902a9f,b5cdd19..eaabd6c
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/IndexedResourceBundle.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/IndexedResourceBundle.java
@@@ -30,7 -30,9 +30,8 @@@ import java.util.ResourceBundle
  import java.util.logging.Level;
  import java.util.logging.LogRecord;
  import java.lang.reflect.Modifier;
+ import javax.measure.Unit;
  import org.opengis.util.CodeList;
 -import org.opengis.util.ControlledVocabulary;
  import org.opengis.util.InternationalString;
  import org.apache.sis.util.Debug;
  import org.apache.sis.util.Classes;
diff --cc ide-project/NetBeans/nbproject/genfiles.properties
index 2505775,74bf923..f2edd02
--- a/ide-project/NetBeans/nbproject/genfiles.properties
+++ b/ide-project/NetBeans/nbproject/genfiles.properties
@@@ -3,6 -3,6 +3,6 @@@
  build.xml.data.CRC32=58e6b21c
  build.xml.script.CRC32=462eaba0
  build.xml.stylesheet.CRC32=28e38971@1.53.1.46
- nbproject/build-impl.xml.data.CRC32=632785c4
 -nbproject/build-impl.xml.data.CRC32=aff02101
 -nbproject/build-impl.xml.script.CRC32=aa8f5386
++nbproject/build-impl.xml.data.CRC32=c09a569b
 +nbproject/build-impl.xml.script.CRC32=3a1dc6ad
  nbproject/build-impl.xml.stylesheet.CRC32=3a2fa800@1.89.1.48
diff --cc pom.xml
index 94546f8,a0af729..a5ccae8
--- a/pom.xml
+++ b/pom.xml
@@@ -524,8 -524,8 +524,8 @@@
      <maven.compiler.source>8</maven.compiler.source>
      <maven.compiler.target>8</maven.compiler.target>
      <sis.plugin.version>${project.version}</sis.plugin.version>
-     <sis.non-free.version>1.0-M1</sis.non-free.version>
+     <sis.non-free.version>1.0-M1</sis.non-free.version>                 <!-- Used only if "non-free" profile is enabled. -->
 -    <geoapi.version>3.1-SNAPSHOT</geoapi.version>
 +    <geoapi.version>3.0.1</geoapi.version>
    </properties>
  
    <profiles>
diff --cc storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Axis.java
index 8d41055,830d663..79b11c6
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Axis.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Axis.java
@@@ -104,24 -202,259 +202,259 @@@ public final class Axis extends NamedEl
      }
  
      /**
-      * Returns the axis direction for the given unit of measurement as a sign relative to the given direction.
+      * Returns the axis direction for the given unit of measurement, or {@code null} if unknown.
       * This method performs the second half of the work of parsing "degrees_east" or "degrees_west" units.
       *
-      * @param  unit      the string representation of the netCDF unit, or {@code null}.
-      * @param  positive  the direction to take as positive value: {@link AxisDirection#EAST} or {@link AxisDirection#NORTH}.
-      * @return the axis direction as a sign relative to the positive direction, or 0 if unrecognized.
+      * @param  unit  the string representation of the netCDF unit, or {@code null}.
+      * @return the axis direction, or {@code null} if unrecognized.
       */
-     public static int direction(final String unit, final AxisDirection positive) {
+     public static AxisDirection direction(final String unit) {
          if (unit != null) {
-             final int s = unit.indexOf('_');
+             int s = unit.indexOf('_');
+             if (s < 0) {
+                 s = unit.indexOf(' ');
+             }
              if (s > 0) {
-                 final AxisDirection dir = Types.forCodeName(AxisDirection.class, unit.substring(s+1), false);
-                 if (dir != null) {
-                     if (dir.equals(positive)) return +1;
-                     if (dir.equals(AxisDirections.opposite(positive))) return -1;
+                 return Types.forCodeName(AxisDirection.class, unit.substring(s+1), false);
+             }
+         }
+         return null;
+     }
+ 
+     /**
+      * Returns the name of this axis.
+      *
+      * @return the name of this element.
+      */
+     @Override
+     public final String getName() {
+         return coordinates.getName().trim();
+     }
+ 
+     /**
+      * Returns the unit of measurement of this axis, or {@code null} if unknown.
+      *
+      * @return the unit of measurement, or {@code null} if unknown.
+      */
+     public final Unit<?> getUnit() {
+         return coordinates.getUnit();
+     }
+ 
+     /**
+      * Returns {@code true} if the given axis specifies the same direction and unit of measurement than this axis.
+      * This is used for testing is a predefined axis can be used instead than invoking {@link #toISO(CSFactory)}.
+      */
+     final boolean isSameUnitAndDirection(final CoordinateSystemAxis axis) {
+         return axis.getDirection().equals(direction) && axis.getUnit().equals(getUnit());
+     }
+ 
+     /**
+      * Returns {@code true} if coordinates in this axis seem to map cell corner instead than cell center.
+      * A {@code false} value does not necessarily means that the axis maps cell center; it can be unknown.
+      * This method assumes a geographic CRS.
+      *
+      * <p>From CF-Convention: <cite>"If bounds are not provided, an application might reasonably assume the
+      * grid points to be at the centers of the cells, but we do not require that in this standard."</cite>
+      * We nevertheless tries to guess by checking if the "cell center" convention would result in coordinates
+      * outside the range of longitude or latitude values.</p>
+      */
+     final boolean isCellCorner() throws IOException, DataStoreException {
+         double min;
+         boolean wraparound;
+         switch (abbreviation) {
+             case 'λ': min = Longitude.MIN_VALUE; wraparound = true;  break;
+             case 'φ': min =  Latitude.MIN_VALUE; wraparound = false; break;
+             default: return false;
+         }
+         final Vector data = coordinates.read();
+         final int size = data.size();
+         if (size != 0) {
+             Unit<?> unit = getUnit();
+             if (unit == null) {
+                 unit = Units.DEGREE;
+             }
+             try {
+                 final UnitConverter uc = unit.getConverterToAny(Units.DEGREE);
+                 if (wraparound && uc.convert(data.doubleValue(size - 1)) > Longitude.MAX_VALUE) {
+                     min = 0;            // Replace [-180 … +180]° longitude range by [0 … 360]°.
+                 }
+                 return uc.convert(data.doubleValue(0)) == min;
+             } catch (IncommensurableException e) {
+                 coordinates.error(Grid.class, "getGridGeometry", e, Errors.Keys.InconsistentUnitsForCS_1, unit);
+             }
+         }
+         return false;
+     }
+ 
+     /**
+      * Compares this axis with the given axis for ordering based on the dimensions declared in a variable.
+      * This is used for sorting axes in the same order than the dimension order on the variable using axes.
+      * This order is not necessarily the same than the order in which variables are listed in the netCDF file.
+      *
+      * @param  other  the other axis to compare to this axis.
+      * @return -1 if this axis should be before {@code other}, +1 if it should be after.
+      */
+     @Override
+     public int compareTo(final Axis other) {
+         return sourceDimensions[0] - other.sourceDimensions[0];
+     }
+ 
+     /**
+      * Creates an ISO 19111 axis from the information stored in this netCDF axis.
+      *
+      * @param  factory  the factory to use for creating the coordinate system axis.
+      * @return the ISO axis.
+      */
+     final CoordinateSystemAxis toISO(final CSFactory factory) throws FactoryException {
+         /*
+          * The axis name is stored without namespace, because the variable name in a netCDF file can be anything;
+          * this is not controlled vocabulary. However the standard name, if any, is stored with "NetCDF" namespace
+          * because this is controlled vocabulary.
+          */
+         final String name = getName();
+         final Map<String,Object> properties = new HashMap<>(4);
+         properties.put(CoordinateSystemAxis.NAME_KEY, name);                        // Intentionally no namespace.
+         final List<GenericName> aliases = new ArrayList<>(2);
+         final String standardName = coordinates.getAttributeAsString(CF.STANDARD_NAME);
+         if (standardName != null) {
+             final NamedIdentifier std = new NamedIdentifier(Citations.NETCDF, standardName);
+             if (standardName.equals(name)) {
+                 properties.put(CoordinateSystemAxis.NAME_KEY, std);                 // Store as primary name.
+             } else {
+                 aliases.add(std);                                                   // Store as alias.
+             }
+         }
+         /*
+          * The long name is stored as an optional description of the primary name.
+          * It is also stored as an alias if not redundant with other names.
+          */
+         final String alt = coordinates.getAttributeAsString(CDM.LONG_NAME);
+         if (alt != null && !similar(alt, name)) {
 -            properties.put(org.opengis.metadata.Identifier.DESCRIPTION_KEY, alt);   // Description associated to primary name.
++            properties.put(org.apache.sis.metadata.iso.ImmutableIdentifier.DESCRIPTION_KEY, alt);   // Description associated to primary name.
+             if (!similar(alt, standardName)) {
+                 aliases.add(new NamedIdentifier(null, alt));                        // Additional alias.
+             }
+         }
+         if (!aliases.isEmpty()) {
+             properties.put(CoordinateSystemAxis.ALIAS_KEY, aliases.toArray(new GenericName[aliases.size()]));
+         }
+         /*
+          * Axis abbreviation, direction and unit of measurement are mandatory. If any of them is null,
+          * creation of CoordinateSystemAxis is likely to fail with an InvalidGeodeticParameterException.
+          * We provide default values for the most well-accepted values and leave other values to null.
+          * Those null values can be accepted if users specify their own factory.
+          */
+         Unit<?> unit = getUnit();
+         if (unit == null) {
+             switch (abbreviation) {
+                 /*
+                  * TODO: consider moving those default values in a separated class,
+                  * for example a netCDF-specific CSFactory, for allowing users to override.
+                  */
+                 case 'λ': case 'φ': unit = Units.DEGREE; break;
+             }
+         }
+         final String abbr;
+         if (abbreviation != 0) {
+             abbr = Character.toString(abbreviation).intern();
+         } else if (direction != null && unit != null) {
+             abbr = AxisDirections.suggestAbbreviation(name, direction, unit);
+         } else {
+             abbr = null;
+         }
+         return factory.createCoordinateSystemAxis(properties, abbr, direction, unit);
+     }
+ 
+     /**
+      * Sets the scale and offset coefficients in the given "grid to CRS" transform if possible.
+      * Source and target dimensions used by this method are in "natural" order (reverse of netCDF order).
+      * Setting the coefficient is possible only if values in this variable are regular,
+      * i.e. the difference between two consecutive values is constant.
+      *
+      * <p>If this method returns {@code true}, then the {@code nonLinears} list is left unchanged.
+      * If this method returns {@code false}, then a non-linear transform or {@code null} has been
+      * added to the {@code nonLinears} list.</p>
+      *
+      * @param  gridToCRS   the matrix in which to set scale and offset coefficient.
+      * @param  srcEnd      number of source dimensions (grid dimensions) - 1. Identifies the last column in the matrix.
+      * @param  tgtDim      the target dimension, which is a dimension of the CRS. Identifies the matrix row of scale factor.
+      * @param  nonLinears  where to add a non-linear transform if we can not compute a linear one. {@code null} may be added.
+      * @return whether this method successfully set the scale and offset coefficients.
+      * @throws IOException if an error occurred while reading the data.
+      * @throws DataStoreException if a logical error occurred.
+      */
+     final boolean trySetTransform(final Matrix gridToCRS, final int srcEnd, final int tgtDim,
+             final List<MathTransform> nonLinears) throws IOException, DataStoreException
+     {
+         switch (sourceDimensions.length) {
+             /*
+              * Defined as a matter of principle, but should never happen.
+              */
+             case 0: return true;
+             /*
+              * Normal case where the axis has only one dimension.
+              */
+             case 1: {
+                 final int srcDim = srcEnd - sourceDimensions[0];
+                 if (coordinates.trySetTransform(gridToCRS, srcDim, tgtDim, null)) {
+                     return true;
+                 } else {
+                     nonLinears.add(MathTransforms.interpolate(null, coordinates.read().doubleValues()));
+                     return false;
+                 }
+             }
+             /*
+              * In netCDF files, axes are sometime associated to two-dimensional localization grids.
+              * If this is the case, then the following block checks if we can reduce those grids to
+              * one-dimensional vector. For example the following localisation grids:
+              *
+              *    10 10 10 10                  10 12 15 20
+              *    12 12 12 12        or        10 12 15 20
+              *    15 15 15 15                  10 12 15 20
+              *    20 20 20 20                  10 12 15 20
+              *
+              * can be reduced to a one-dimensional {10 12 15 20} vector (orientation matter however).
+              *
+              * Note: following block is currently restricted to the two-dimensional case, but it could
+              * be generalized to n-dimensional case if we resolve the default case in the switch statement.
+              */
+             case 2: {
+                 Vector data = coordinates.read();
+                 final int[] repetitions = data.repetitions(sourceSizes);    // Detects repetitions as illustrated above.
+                 if (repetitions.length != 0) {
+                     for (int i=0; i<sourceDimensions.length; i++) {
+                         final int srcDim = srcEnd - sourceDimensions[i];    // "Natural" order is reverse of netCDF order.
+                         final int length = sourceSizes[i];
+                         int step = 1;
+                         for (int j=0; j<sourceDimensions.length; j++) {
+                             int previous = srcEnd - sourceDimensions[j];
+                             if (previous < srcDim) step *= sourceSizes[j];
+                         }
+                         final boolean condition;
+                         switch (srcDim) {
+                             case 0:  condition = repetitions.length > 1 && (repetitions[1] % length) == 0; break;
+                             case 1:  condition =                           (repetitions[0] % step)   == 0; break;
+                             default: throw new AssertionError();        // I don't know yet how to generalize to n dimensions.
+                         }
+                         if (condition) {                                // Repetition length shall be grid size (or a multiple).
+                             data = data.subSampling(0, step, length);
+                             if (coordinates.trySetTransform(gridToCRS, srcDim, tgtDim, data)) {
+                                 return true;
+                             } else {
+                                 nonLinears.add(MathTransforms.interpolate(null, data.doubleValues()));
+                                 return false;
+                             }
+                         }
+                     }
                  }
              }
+             /*
+              * Localization grid of 3 dimensions or more are theoretically possible, but uncommon.
+              * Such grids are not yet supported. Note that we can read three or more dimensional data;
+              * it is only the localization grid which is limited to one or two dimensions.
+              */
          }
-         return 0;
+         nonLinears.add(null);
+         return false;
      }
  }
diff --cc storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Decoder.java
index 60a5da1,90756d7..b0538a2
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Decoder.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Decoder.java
@@@ -28,10 -30,8 +30,11 @@@ import org.apache.sis.storage.DataStore
  import org.apache.sis.storage.DataStoreException;
  import org.apache.sis.util.logging.WarningListeners;
  import org.apache.sis.internal.system.DefaultFactories;
+ import org.apache.sis.internal.referencing.ReferencingFactoryContainer;
  
 +// Branch-dependent imports
 +import org.apache.sis.util.iso.DefaultNameFactory;
 +
  
  /**
   * The API used internally by Apache SIS for fetching variables and attribute values from a netCDF file.
@@@ -85,7 -104,8 +107,8 @@@ public abstract class Decoder extends R
          Objects.requireNonNull(listeners);
          this.geomlib     = geomlib;
          this.listeners   = listeners;
 -        this.nameFactory = DefaultFactories.forBuildin(NameFactory.class);
 +        this.nameFactory = DefaultFactories.forBuildin(NameFactory.class, DefaultNameFactory.class);
+         this.datumCache  = new Datum[CRSBuilder.DATUM_CACHE_SIZE];
      }
  
      /**
diff --cc storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/FeaturesInfo.java
index c5fa53b,75402e8..4095467
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/FeaturesInfo.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/FeaturesInfo.java
@@@ -339,11 -343,11 +341,11 @@@ search: for (final VariableInfo counts 
           * @todo current reading process implies lot of seeks, which is inefficient.
           */
          @Override
 -        public boolean tryAdvance(final Consumer<? super Feature> action) {
 +        public boolean tryAdvance(final Consumer<? super AbstractFeature> action) {
-             final int   length = counts.intValue(index);
-             final int[] lower  = {position};
-             final int[] upper  = {position + length};
-             final int[] step   = {1};
+             final int length = counts.intValue(index);
+             final GridExtent extent = new GridExtent(null, new long[] {position},
+                             new long[] {Math.addExact(position, length)}, false);
+             final int[] step = {1};
              final Vector   id, t;
              final Vector[] coords = new Vector[coordinates.length];
              final Object[] props  = new Object[properties.length];
diff --cc storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/FeaturesWrapper.java
index 2615bbe,4072b5c..4efdb52
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/FeaturesWrapper.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/FeaturesWrapper.java
@@@ -56,12 -55,7 +55,7 @@@ final class FeaturesWrapper extends Dis
      }
  
      @Override
-     public GenericName getIdentifier() {
-         throw new UnsupportedOperationException();      // TODO
-     }
- 
-     @Override
 -    public FeatureType getType() {
 +    public DefaultFeatureType getType() {
          throw new UnsupportedOperationException();      // TODO
      }
  
diff --cc storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java
index 75b665e,148233b..a9f7f47
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java
@@@ -46,9 -47,9 +46,10 @@@ import org.opengis.metadata.maintenance
  import org.opengis.metadata.constraint.Restriction;
  import org.opengis.referencing.cs.AxisDirection;
  import org.opengis.referencing.crs.VerticalCRS;
+ import org.opengis.referencing.crs.CoordinateReferenceSystem;
  
  import org.apache.sis.util.iso.Types;
 +import org.apache.sis.util.iso.DefaultNameFactory;
  import org.apache.sis.util.logging.WarningListeners;
  import org.apache.sis.storage.DataStore;
  import org.apache.sis.storage.DataStoreException;
@@@ -921,14 -936,12 +929,12 @@@ split:  while ((start = CharSequences.s
          newSampleDimension();
          final String name = trim(variable.getName());
          if (name != null) {
 -            final NameFactory f = decoder.nameFactory;
 +            final DefaultNameFactory f = decoder.nameFactory;
              setBandIdentifier(f.createMemberName(null, name, f.createTypeName(null, variable.getDataTypeName())));
          }
-         Object[] v = variable.getAttributeValues(CF.STANDARD_NAME, false);
-         final String id = (v.length == 1) ? trim((String) v[0]) : null;
+         final String id = trim(variable.getAttributeAsString(CF.STANDARD_NAME));
          if (id != null && !id.equals(name)) {
-             v = variable.getAttributeValues(ACDD.standard_name_vocabulary, false);
-             addBandName(v.length == 1 ? (String) v[0] : null, id);
+             addBandName(variable.getAttributeAsString(ACDD.standard_name_vocabulary), id);
          }
          final String description = trim(variable.getDescription());
          if (description != null && !description.equals(name) && !description.equals(id)) {
diff --cc storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/GridTest.java
index daebc6f,62b12ca..bce7673
--- a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/GridTest.java
+++ b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/GridTest.java
@@@ -18,9 -18,9 +18,8 @@@ package org.apache.sis.internal.netcdf
  
  import java.io.IOException;
  import org.apache.sis.storage.DataStoreException;
- import org.apache.sis.storage.netcdf.AttributeNames;
  import org.apache.sis.test.DependsOn;
  import org.apache.sis.test.DependsOnMethod;
 -import org.opengis.test.dataset.TestData;
  import org.junit.Test;
  
  import static org.junit.Assert.*;
diff --cc storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/impl/GridInfoTest.java
index c6fd0d3,eb23e9b..ab80436
--- a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/impl/GridInfoTest.java
+++ b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/impl/GridInfoTest.java
@@@ -24,11 -23,12 +23,14 @@@ import org.apache.sis.internal.netcdf.G
  import org.apache.sis.storage.DataStoreException;
  import org.apache.sis.util.ArraysExt;
  import org.apache.sis.test.DependsOn;
 -import org.opengis.test.dataset.TestData;
 +
++// Branch-specific imports
++import org.apache.sis.internal.netcdf.TestData;
+ 
  
  /**
-  * Tests the {@link GridGeometry} implementation. This test shall be executed only if the
-  * {@link GridGeometryTest} tests, which use the UCAR library has a reference implementation,
+  * Tests the {@link GridInfo} implementation. This test shall be executed only if the
+  * {@link GridTest} tests, which use the UCAR library has a reference implementation,
   * passed.
   *
   * @author  Martin Desruisseaux (Geomatys)
diff --cc storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractFeatureSet.java
index 70fd76a,fe7ad8c..e548f0f
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractFeatureSet.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractFeatureSet.java
@@@ -70,32 -72,44 +72,44 @@@ public abstract class AbstractFeatureSe
      }
  
      /**
-      * Returns a description of this set of features.
-      * Current implementation sets only the resource name; this may change in any future Apache SIS version.
+      * Returns the feature type name as the identifier for this resource.
       *
-      * <div class="note"><b>Note:</b>
-      * we currently do not set the geographic extent from the envelope because default {@link #getEnvelope()}
-      * implementation itself invokes {@code getMetadata()}. Consequently requesting the envelope from this
-      * method could create a never-ending loop.</div>
+      * @return the resource identifier inferred from metadata, or {@code null} if none or ambiguous.
+      * @throws DataStoreException if an error occurred while fetching the identifier.
+      *
+      * @see DataStore#getIdentifier()
       */
      @Override
-     public synchronized Metadata getMetadata() throws DataStoreException {
-         if (metadata == null) {
-             final DefaultMetadata metadata = new DefaultMetadata();
-             final DefaultFeatureType type = getType();
-             if (type != null) {
-                 final GenericName name = type.getName();
-                 if (name != null) {                         // Paranoiac check (should never be null).
-                     final DefaultCitation citation = new DefaultCitation(name.toInternationalString());
-                     final DefaultDataIdentification identification = new DefaultDataIdentification();
-                     identification.setCitation(citation);
-                 }
-             }
-             // No geographic extent - see above javadoc.
-             metadata.transition(DefaultMetadata.State.FINAL);
-             this.metadata = metadata;
+     public GenericName getIdentifier() throws DataStoreException {
 -        final FeatureType type = getType();
++        final DefaultFeatureType type = getType();
+         if (type != null) {
+             return type.getName();
          }
-         return metadata;
+         return null;
+     }
+ 
+     /**
+      * Returns an estimation of the number of features in this set, or {@code null} if unknown.
+      * The default implementation returns {@code null}.
+      *
+      * @return estimation of the number of features, or {@code null}.
+      */
+     protected Integer getFeatureCount() {
+         return null;
+     }
+ 
+     /**
+      * Invoked the first time that {@link #getMetadata()} is invoked. The default implementation populates metadata
+      * based on information provided by {@link #getType()}, {@link #getIdentifier()} and {@link #getEnvelope()}.
+      * Subclasses should override if they can provide more information.
+      *
+      * @param  metadata  the builder where to set metadata properties.
+      * @throws DataStoreException if an error occurred while reading metadata from the data store.
+      */
+     @Override
+     protected void createMetadata(final MetadataBuilder metadata) throws DataStoreException {
+         super.createMetadata(metadata);
+         metadata.addFeatureType(getType(), getFeatureCount());
      }
  
      /**
diff --cc storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MemoryFeatureSet.java
index b9b1c82,fb21125..fca1d07
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MemoryFeatureSet.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MemoryFeatureSet.java
@@@ -60,9 -58,8 +58,8 @@@ public class MemoryFeatureSet extends A
       * @param type       the type of all features in the given collection.
       * @param features   collection of stored features. This collection will not be copied.
       */
-     public MemoryFeatureSet(final WarningListeners<DataStore> listeners, Metadata metadata,
+     public MemoryFeatureSet(final WarningListeners<DataStore> listeners,
 -                            final FeatureType type, final Collection<Feature> features)
 +                            final DefaultFeatureType type, final Collection<AbstractFeature> features)
      {
          super(listeners);
          ArgumentChecks.ensureNonNull("type",     type);
@@@ -72,14 -69,13 +69,13 @@@
      }
  
      /**
-      * Returns the name of the feature type.
+      * Returns the type common to all feature instances in this set.
       *
-      * @return feature type name.
+      * @return a description of properties that are common to all features in this dataset.
       */
      @Override
-     public GenericName getIdentifier() {
-         return type.getName();
 -    public FeatureType getType() {
++    public DefaultFeatureType getType() {
+         return type;
      }
  
      /**
diff --cc storage/sis-storage/src/main/java/org/apache/sis/internal/storage/StoreUtilities.java
index 66a3edb,2557a06..9317a7f
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/StoreUtilities.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/StoreUtilities.java
@@@ -39,7 -44,7 +44,8 @@@ import org.apache.sis.util.CharSequence
  import org.apache.sis.util.Classes;
  
  // Branch-dependent imports
 -import org.opengis.feature.Feature;
 +import org.apache.sis.feature.AbstractFeature;
++import org.apache.sis.metadata.iso.identification.AbstractIdentification;
  
  
  /**
@@@ -141,6 -146,36 +147,39 @@@ public final class StoreUtilities exten
      }
  
      /**
+      * Returns the spatio-temporal envelope of the given metadata.
+      * This method computes the union of all {@link GeographicBoundingBox} in the metadata, assuming the
+      * {@linkplain org.apache.sis.referencing.CommonCRS#defaultGeographic() default geographic CRS}
+      * (usually WGS 84).
+      *
+      * @param  metadata  the metadata from which to compute the envelope, or {@code null}.
+      * @return the spatio-temporal extent, or {@code null} if none.
+      */
+     public static Envelope getEnvelope(final Metadata metadata) {
+         GeneralEnvelope bounds = null;
+         if (metadata != null) {
+             for (final Identification identification : metadata.getIdentificationInfo()) {
 -                for (final Extent extent : identification.getExtents()) {
++                if (!(identification instanceof AbstractIdentification)) {
++                    continue;       // Following cast is specific to GeoAPI 3.0 branch.
++                }
++                for (final Extent extent : ((AbstractIdentification) identification).getExtents()) {
+                     for (final GeographicExtent ge : extent.getGeographicElements()) {
+                         if (ge instanceof GeographicBoundingBox) {
+                             final GeneralEnvelope env = new GeneralEnvelope((GeographicBoundingBox) ge);
+                             if (bounds == null) {
+                                 bounds = env;
+                             } else {
+                                 bounds.add(env);
+                             }
+                         }
+                     }
+                 }
+             }
+         }
+         return bounds;
+     }
+ 
+     /**
       * Returns the most specific interface implemented by the given class.
       * For indicative purpose only, as this method has arbitrary behavior if more than one leaf is found.
       *
diff --cc storage/sis-storage/src/main/java/org/apache/sis/internal/storage/query/FeatureSubset.java
index fa6db66,9fe1f5f..c327d87
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/query/FeatureSubset.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/query/FeatureSubset.java
@@@ -16,9 -16,10 +16,8 @@@
   */
  package org.apache.sis.internal.storage.query;
  
 -import java.util.List;
  import java.util.stream.Stream;
  import org.opengis.util.GenericName;
- import org.opengis.geometry.Envelope;
 -import org.apache.sis.internal.feature.FeatureUtilities;
  import org.apache.sis.internal.storage.AbstractFeatureSet;
  import org.apache.sis.storage.DataStoreException;
  import org.apache.sis.storage.FeatureSet;
diff --cc storage/sis-storage/src/main/java/org/apache/sis/storage/DataStore.java
index 510ea56,0889df2..156f6d7
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/DataStore.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/DataStore.java
@@@ -28,10 -31,10 +31,13 @@@ import org.apache.sis.util.Localized
  import org.apache.sis.util.ArgumentChecks;
  import org.apache.sis.util.logging.WarningListener;
  import org.apache.sis.util.logging.WarningListeners;
- import org.apache.sis.internal.storage.AbstractResource;
  import org.apache.sis.internal.storage.StoreUtilities;
  import org.apache.sis.internal.storage.Resources;
+ import org.apache.sis.referencing.NamedIdentifier;
+ 
++// Branch-specific imports
++import org.opengis.referencing.ReferenceIdentifier;
 +
  
  /**
   * Manages a series of features, coverages or sensor data.
@@@ -292,7 -295,31 +298,31 @@@ public abstract class DataStore impleme
       */
      @Override
      public GenericName getIdentifier() throws DataStoreException {
-         return AbstractResource.identifier(getMetadata());
+         final Metadata metadata = getMetadata();
+         if (metadata != null) {
+             Citation citation = null;
+             for (final Identification id : metadata.getIdentificationInfo()) {
+                 final Citation c = id.getCitation();
+                 if (c != null) {
+                     if (citation != null && citation != c) return null;                 // Ambiguity.
+                     citation = c;
+                 }
+             }
+             if (citation != null) {
 -                Identifier first = null;
++                ReferenceIdentifier first = null;
+                 for (final Identifier c : citation.getIdentifiers()) {
+                     if (c instanceof GenericName) {
+                         return (GenericName) c;
 -                    } else if (first == null) {
 -                        first = c;
++                    } else if (first == null && c instanceof ReferenceIdentifier) {
++                        first = (ReferenceIdentifier) c;
+                     }
+                 }
+                 if (first != null) {
+                     return new NamedIdentifier(first);
+                 }
+             }
+         }
+         return null;
      }
  
      /**


Mime
View raw message