sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 04/05: Add GridGeometry.reduce(lower, upper) method.
Date Thu, 03 Jan 2019 16:01:50 GMT
This is an automated email from the ASF dual-hosted git repository.

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

commit ed2e54e6eca21fab58b021bd2a21ac6a2125c7c9
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Thu Jan 3 16:49:33 2019 +0100

    Add GridGeometry.reduce(lower, upper) method.
---
 .../metadata/EllipsoidalHeightCombiner.java        |   9 +-
 .../org/apache/sis/coverage/grid/GridExtent.java   |  18 ++-
 .../org/apache/sis/coverage/grid/GridGeometry.java | 164 ++++++++++++++++++---
 .../apache/sis/coverage/grid/GridExtentTest.java   |   6 +-
 .../apache/sis/coverage/grid/GridGeometryTest.java |  18 +--
 .../apache/sis/internal/referencing/Resources.java |   5 +
 .../sis/internal/referencing/Resources.properties  |   1 +
 .../internal/referencing/Resources_fr.properties   |   1 +
 .../main/java/org/apache/sis/referencing/CRS.java  |  89 +++++++++++
 .../java/org/apache/sis/referencing/CommonCRS.java |  49 +++---
 .../referencing/EllipsoidalHeightSeparator.java    | 136 +++++++++++++++++
 .../org/apache/sis/referencing/cs/AbstractCS.java  |   2 +
 .../sis/referencing/cs/CoordinateSystems.java      |   4 +-
 .../sis/referencing/cs/DefaultCartesianCS.java     |   1 +
 .../sis/referencing/cs/DefaultEllipsoidalCS.java   |   2 +-
 .../java/org/apache/sis/referencing/CRSTest.java   |  39 ++++-
 .../apache/sis/referencing/crs/HardCodedCRS.java   |   8 +-
 17 files changed, 490 insertions(+), 62 deletions(-)

diff --git a/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/EllipsoidalHeightCombiner.java b/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/EllipsoidalHeightCombiner.java
index 827826c..d6d4a7e 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/EllipsoidalHeightCombiner.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/EllipsoidalHeightCombiner.java
@@ -40,12 +40,15 @@ import org.apache.sis.util.ArraysExt;
 
 
 /**
- * A class in charges of combining two-dimensional geographic or projected CRS with an ellipsoidal height
- * into a three-dimensional CRS.
+ * A class in charges of combining two-dimensional geographic or projected CRS with an ellipsoidal height into a
+ * three-dimensional CRS. This is the converse of {@link org.apache.sis.referencing.EllipsoidalHeightSeparator}.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 0.8
- * @since   0.8
+ *
+ * @see org.apache.sis.referencing.EllipsoidalHeightSeparator
+ *
+ * @since 0.8
  * @module
  */
 public class EllipsoidalHeightCombiner {
diff --git 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
index b1a1f9b..4c6318e 100644
--- 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
@@ -751,17 +751,22 @@ public class GridExtent implements Serializable {
     }
 
     /**
-     * Returns a new grid envelope that encompass only some dimensions of this grid envelope.
+     * Returns a grid envelope that encompass only some dimensions of this grid envelope.
      * This method copies this grid envelope into a new grid envelope, beginning at dimension
      * {@code lower} and extending to dimension {@code upper-1} inclusive. Thus the dimension
      * of the sub grid envelope is {@code upper - lower}.
      *
+     * <p>This method performs a <cite>dimensionality reduction</cite> and can be used as the
+     * converse of {@link #append(DimensionNameType, long, long, boolean)}.</p>
+     *
      * @param  lower  the first dimension to copy, inclusive.
      * @param  upper  the last  dimension to copy, exclusive.
      * @return the sub-envelope, or {@code this} if [{@code lower} … {@code upper}] is [0 … {@link #getDimension() dimension}].
      * @throws IndexOutOfBoundsException if an index is out of bounds.
+     *
+     * @see GridGeometry#reduce(int, int)
      */
-    public GridExtent subExtent(final int lower, final int upper) {
+    public GridExtent reduce(final int lower, final int upper) {
         final int dimension = getDimension();
         ArgumentChecks.ensureValidIndexRange(dimension, lower, upper);
         final int newDim = upper - lower;
@@ -791,6 +796,9 @@ public class GridExtent implements Serializable {
      * which implies that accurate representation of the same envelope may require fractional cells on some
      * grid borders.</div>
      *
+     * This method does not reduce the number of dimensions of the grid extent.
+     * For dimensionality reduction, see {@link #reduce(int, int)}.
+     *
      * @param  strides  the strides. Length shall be equal to the number of dimension and all values shall be greater than zero.
      * @return the sub-sampled extent, or {@code this} is sub-sampling results in the same extent.
      * @throws IllegalArgumentException if a stride is not greater than zero.
@@ -824,7 +832,11 @@ public class GridExtent implements Serializable {
      * Creates a new grid extent which represent a slice of this grid at the given point.
      * The given point may have less dimensions than this grid extent, in which case the
      * dimensions must be specified in the {@code modifiedDimensions} array. Coordinates
-     * of the given point will be rounded to nearest integer.
+     * in the given point will be rounded to nearest integer. This method does not reduce
+     * the number of dimensions of the grid extent.
+     *
+     * <p>This method does not reduce the number of dimensions of the grid extent.
+     * For dimensionality reduction, see {@link #reduce(int, int)}.</p>
      *
      * @param  slicePoint           where to take a slice. NaN values are handled as if their dimensions were absent.
      * @param  modifiedDimensions   mapping from {@code slicePoint} dimensions to this {@code GridExtent} dimensions,
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
index a376d04..3d4c2cc 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
@@ -23,6 +23,7 @@ import java.io.Serializable;
 import java.io.IOException;
 import java.io.UncheckedIOException;
 import java.awt.image.RenderedImage;            // For javadoc only.
+import org.opengis.util.FactoryException;
 import org.opengis.metadata.Identifier;
 import org.opengis.geometry.Envelope;
 import org.opengis.geometry.DirectPosition;
@@ -41,6 +42,7 @@ import org.apache.sis.geometry.ImmutableEnvelope;
 import org.apache.sis.referencing.IdentifiedObjects;
 import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.referencing.operation.transform.TransformSeparator;
 import org.apache.sis.referencing.operation.transform.PassThroughTransform;
 import org.apache.sis.internal.referencing.DirectPositionView;
 import org.apache.sis.internal.system.Modules;
@@ -53,6 +55,7 @@ import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.CharSequences;
+import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.Classes;
 import org.apache.sis.util.Debug;
 import org.apache.sis.io.TableAppender;
@@ -181,6 +184,7 @@ public class GridGeometry implements Serializable {
     /**
      * An <em>estimation</em> of the grid resolution, in units of the CRS axes.
      * Computed from {@link #gridToCRS}, eventually together with {@link #extent}.
+     * May be {@code null} if unknown.
      *
      * @see #RESOLUTION
      * @see #getResolution(boolean)
@@ -216,7 +220,7 @@ public class GridGeometry implements Serializable {
     /**
      * Creates a new grid geometry derived from the given grid geometry with a new extent and a modified transform.
      * This constructor is used for creating a grid geometry over a subregion (for example with the grid extent
-     * computed by {@link #getExtent(Envelope)}) or grid geometry for a sub-sampled raster.
+     * computed by {@link #subExtent(Envelope)}) or grid geometry for a sub-sampled raster.
      *
      * <p>If {@code toOther} is non-null, it should be a transform from the given {@code extent} coordinates to the
      * {@code other} grid coordinates. That transform should be merely a {@linkplain MathTransforms#scale(double...)
@@ -235,7 +239,7 @@ public class GridGeometry implements Serializable {
      * @throws NullPointerException if {@code extent} is {@code null} and the other grid geometry contains no other information.
      * @throws TransformException if the math transform can not compute the geospatial envelope from the grid extent.
      *
-     * @see #getExtent(Envelope)
+     * @see #subExtent(Envelope)
      * @see #subgrid(Envelope, double...)
      */
     GridGeometry(final GridGeometry other, final GridExtent extent, final MathTransform toOther) throws TransformException {
@@ -476,6 +480,68 @@ public class GridGeometry implements Serializable {
     }
 
     /**
+     * Creates a new grid geometry over the specified range of dimensions of the given grid geometry.
+     *
+     * @param  other  the grid geometry to copy.
+     * @param  lower  the first dimension to copy, inclusive.
+     * @param  upper  the last  dimension to copy, exclusive.
+     * @throws FactoryException if an error occurred while separating the "grid to CRS" transform.
+     *
+     * @see #reduce(int, int)
+     */
+    private GridGeometry(final GridGeometry other, final int lower, final int upper) throws FactoryException {
+        extent = (other.extent != null) ? other.extent.reduce(lower, upper) : null;
+        final int n = upper - lower;
+        final int[] dimensions;
+        if (other.gridToCRS != null) {
+            TransformSeparator sep = new TransformSeparator(other.gridToCRS);
+            sep.addSourceDimensionRange(lower, upper);
+            gridToCRS  = sep.separate();
+            dimensions = sep.getTargetDimensions();
+            assert dimensions.length == n : Arrays.toString(dimensions);
+
+            sep = new TransformSeparator(other.cornerToCRS);
+            sep.addSourceDimensionRange(lower, upper);
+            sep.addTargetDimensions(dimensions);
+            cornerToCRS = sep.separate();
+            assert Arrays.equals(sep.getSourceDimensions(), dimensions) : Arrays.toString(dimensions);
+        } else {
+            gridToCRS   = null;
+            cornerToCRS = null;
+            dimensions  = ArraysExt.sequence(lower, upper);
+        }
+        final ImmutableEnvelope env = other.envelope;
+        if (env != null) {
+            CoordinateReferenceSystem crs = env.getCoordinateReferenceSystem();
+            crs = org.apache.sis.referencing.CRS.reduce(crs, dimensions);
+            final double[] min = new double[n];
+            final double[] max = new double[n];
+            for (int i=0; i<n; i++) {
+                final int j = dimensions[i];
+                min[i] = env.getLower(j);
+                max[i] = env.getUpper(j);
+            }
+            envelope = new ImmutableEnvelope(min, max, crs);
+        } else {
+            envelope = null;
+        }
+        long     nonLinears = 0;
+        double[] resolution = other.resolution;
+        if (resolution != null) {
+            resolution = new double[n];
+        }
+        for (int i=0; i<n; i++) {
+            final int j = dimensions[i];
+            if (resolution != null) {
+                resolution[i] = other.resolution[j];
+            }
+            nonLinears |= ((other.nonLinears >>> j) & 1L) << i;
+        }
+        this.resolution = resolution;
+        this.nonLinears = nonLinears;
+    }
+
+    /**
      * Returns the number of dimensions of the <em>grid</em>. This is typically the same
      * than the number of {@linkplain #getEnvelope() envelope} dimensions or the number of
      * {@linkplain #getCoordinateReferenceSystem() coordinate reference system} dimensions,
@@ -584,6 +650,9 @@ public class GridGeometry implements Serializable {
      * <p>If the envelope CRS is not specified, then it is assumed the same than the CRS of this grid geometry.
      * In such case the envelope needs to contain all dimensions.</p>
      *
+     * <p>This method does not reduce the number of dimensions of this grid geometry.
+     * For dimensionality reduction, see {@link #reduce(int, int)}.</p>
+     *
      * @param  areaOfInterest  the desired spatiotemporal region in any CRS (transformations will be applied as needed).
      * @return a grid extent of the same dimension than the grid geometry which intersects the given area of interest.
      * @throws IncompleteGridGeometryException if this grid geometry has no extent or no "grid to CRS" transform.
@@ -591,18 +660,37 @@ public class GridGeometry implements Serializable {
      *
      * @see #subgrid(Envelope, double...)
      */
-    public GridExtent getExtent(final Envelope areaOfInterest) throws IncompleteGridGeometryException, TransformException {
+    public GridExtent subExtent(final Envelope areaOfInterest) throws IncompleteGridGeometryException, TransformException {
         ArgumentChecks.ensureNonNull("areaOfInterest", areaOfInterest);
-        if (extent == null) {
-            throw incomplete(EXTENT, Resources.Keys.UnspecifiedGridExtent);
-        }
-        if (cornerToCRS == null) {
-            throw incomplete(GridGeometry.GRID_TO_CRS, Resources.Keys.UnspecifiedTransform);
-        }
+        requireGridToCRS();
         return new SubgridCalculator(this, cornerToCRS, areaOfInterest, null).extent;
     }
 
     /**
+     * Returns the coordinate range of a slice of this grid geometry at the given point.
+     * The given position can be expressed in any coordinate reference system (CRS).
+     * The position should not define a coordinate for all dimensions, otherwise the slice would degenerate
+     * to a single point. Dimensions can be left unspecified either by assigning to {@code slicePoint} a CRS
+     * without those dimensions, or by assigning the NaN value to some coordinates.
+     * See {@link #slice(DirectPosition)} for examples.
+     *
+     * <p>This method does not reduce the number of dimensions of this grid geometry.
+     * For dimensionality reduction, see {@link #reduce(int, int)}.</p>
+     *
+     * @param  slicePoint   the coordinates where to get a slice.
+     * @return a slice of the grid extent at the given slice point.
+     * @throws TransformException if an error occurred while converting the point coordinates to grid coordinates.
+     * @throws PointOutsideCoverageException if the given point is outside the grid extent.
+     *
+     * @see #slice(DirectPosition)
+     */
+    public GridExtent subExtent(final DirectPosition slicePoint) throws TransformException {
+        ArgumentChecks.ensureNonNull("slicePoint", slicePoint);
+        requireGridToCRS();
+        return new SubgridCalculator(this, cornerToCRS, slicePoint).extent;
+    }
+
+    /**
      * Returns the conversion from grid coordinates to "real world" coordinates.
      * The conversion is often an affine transform, but not necessarily.
      * Conversions from cell indices to geospatial coordinates can be performed for example as below:
@@ -872,6 +960,19 @@ public class GridGeometry implements Serializable {
     }
 
     /**
+     * Verifies that this grid geometry defines an {@linkplain #extent} and a {@link #cornerToCRS} transform.
+     * They are the information required for mapping the grid to a spatiotemporal envelope.
+     */
+    private void requireGridToCRS() throws IncompleteGridGeometryException {
+        if (extent == null) {
+            throw incomplete(EXTENT, Resources.Keys.UnspecifiedGridExtent);
+        }
+        if (cornerToCRS == null) {
+            throw incomplete(GRID_TO_CRS, Resources.Keys.UnspecifiedTransform);
+        }
+    }
+
+    /**
      * Returns {@code true} if all the parameters specified by the argument are set.
      * If this method returns {@code true}, then invoking the corresponding getter
      * methods will not throw {@link IncompleteGridGeometryException}.
@@ -911,6 +1012,9 @@ public class GridGeometry implements Serializable {
      * If the length of {@code targetResolution} array is less than the number of dimensions of {@code areaOfInterest},
      * then no sub-sampling will be applied on the missing dimensions.
      *
+     * <p>This method does not reduce the number of dimensions of this grid geometry.
+     * For dimensionality reduction, see {@link #reduce(int, int)}.</p>
+     *
      * @param  areaOfInterest    the desired spatiotemporal region in any CRS (transformations will be applied as needed),
      *                           or {@code null} for not restricting the sub-grid to a sub-area.
      * @param  targetResolution  the desired resolution in the same units and order than the axes of the given envelope,
@@ -919,16 +1023,11 @@ public class GridGeometry implements Serializable {
      * @throws IncompleteGridGeometryException if this grid geometry has no extent or no "grid to CRS" transform.
      * @throws TransformException if an error occurred while converting the envelope coordinates to grid coordinates.
      *
-     * @see #getExtent(Envelope)
+     * @see #subExtent(Envelope)
      * @see GridExtent#subsample(int[])
      */
     public GridGeometry subgrid(final Envelope areaOfInterest, double... targetResolution) throws TransformException {
-        if (extent == null) {
-            throw incomplete(EXTENT, Resources.Keys.UnspecifiedGridExtent);
-        }
-        if (cornerToCRS == null) {
-            throw incomplete(GRID_TO_CRS, Resources.Keys.UnspecifiedTransform);
-        }
+        requireGridToCRS();
         final SubgridCalculator sub = new SubgridCalculator(this, cornerToCRS, areaOfInterest, targetResolution);
         if (sub.toSubsampled != null || sub.extent != extent) {
             return new GridGeometry(this, sub.extent, sub.toSubsampled);
@@ -939,7 +1038,7 @@ public class GridGeometry implements Serializable {
     /**
      * Returns a grid geometry for a slice at the given point.
      * The given position can be expressed in any coordinate reference system (CRS).
-     * The position should not define a coordinate for all dimensions, otherwise the sub-grid would degenerate
+     * The position should not define a coordinate for all dimensions, otherwise the slice would degenerate
      * to a single point. Dimensions can be left unspecified either by assigning to {@code slicePoint} a CRS
      * without those dimensions, or by assigning the NaN value to some coordinates.
      *
@@ -954,18 +1053,49 @@ public class GridGeometry implements Serializable {
      *       of the grid geometry CRS.</li>
      * </ul></div>
      *
+     * This method does not reduce the number of dimensions of this grid geometry.
+     * For dimensionality reduction, see {@link #reduce(int, int)}.
+     *
      * @param  slicePoint   the coordinates where to get a slice.
      * @return a slice of this grid geometry at the given slice point. May be {@code this}.
      * @throws TransformException if an error occurred while converting the point coordinates to grid coordinates.
      * @throws PointOutsideCoverageException if the given point is outside the grid extent.
+     *
+     * @see #subExtent(DirectPosition)
      */
     public GridGeometry slice(final DirectPosition slicePoint) throws TransformException {
         ArgumentChecks.ensureNonNull("slicePoint", slicePoint);
+        requireGridToCRS();
         final GridExtent slice = new SubgridCalculator(this, cornerToCRS, slicePoint).extent;
         return (slice != extent) ? new GridGeometry(this, slice, null) : this;
     }
 
     /**
+     * Returns a grid geometry that encompass only some dimensions of this grid geometry.
+     * This method copies this grid geometry into a new grid geometry, beginning at dimension
+     * {@code lower} and extending to dimension {@code upper-1} inclusive. Thus the dimension
+     * of the sub grid geometry is {@code upper - lower}.
+     *
+     * <p>This method performs a <cite>dimensionality reduction</cite>.</p>
+     *
+     * @param  lower  the first grid dimension to copy, inclusive.
+     * @param  upper  the last  grid dimension to copy, exclusive.
+     * @return the sub grid geometry, or {@code this} if [{@code lower} … {@code upper}] is [0 … {@link #getDimension() dimension}].
+     * @throws IndexOutOfBoundsException if an index is out of bounds.
+     *
+     * @see GridExtent#reduce(int, int)
+     */
+    public GridGeometry reduce(final int lower, final int upper) {
+        if (lower == 0 && upper == getDimension()) {
+            return this;
+        } else try {
+            return new GridGeometry(this, lower, upper);
+        } catch (FactoryException e) {
+            throw new RuntimeException(e);      // TODO
+        }
+    }
+
+    /**
      * Returns a hash value for this grid geometry. This value need not remain
      * consistent between different implementations of the same class.
      */
diff --git a/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridExtentTest.java b/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridExtentTest.java
index b1ad427..01bfeff 100644
--- a/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridExtentTest.java
+++ b/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridExtentTest.java
@@ -122,12 +122,12 @@ public final strictfp class GridExtentTest extends TestCase {
     }
 
     /**
-     * Tests {@link GridExtent#subExtent(int, int)}.
+     * Tests {@link GridExtent#reduce(int, int)}.
      */
     @Test
-    public void testSubExtent() {
+    public void testReduce() {
         GridExtent extent = create3D();
-        extent = extent.subExtent(0, 2);
+        extent = extent.reduce(0, 2);
         assertEquals("dimension", 2, extent.getDimension());
         assertExtentEquals(extent, 0, 100, 499);
         assertExtentEquals(extent, 1, 200, 799);
diff --git a/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridGeometryTest.java b/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridGeometryTest.java
index 3c503bc..f484946 100644
--- a/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridGeometryTest.java
+++ b/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridGeometryTest.java
@@ -248,13 +248,13 @@ public final strictfp class GridGeometryTest extends TestCase {
     }
 
     /**
-     * Tests {@link GridGeometry#getExtent(Envelope)}.
+     * Tests {@link GridGeometry#subExtent(Envelope)}.
      *
      * @throws TransformException if an error occurred while using the "grid to CRS" transform.
      */
     @Test
     @DependsOnMethod("testFromGeospatialEnvelope")
-    public void testGetExtent() throws TransformException {
+    public void testSubExtent() throws TransformException {
         GeneralEnvelope envelope = new GeneralEnvelope(HardCodedCRS.WGS84_3D);
         envelope.setRange(0, -80, 120);
         envelope.setRange(1, -12,  21);
@@ -276,17 +276,17 @@ public final strictfp class GridGeometryTest extends TestCase {
         envelope.setRange(0, -70.001, +80.002);
         envelope.setRange(1,   4.997,  15.003);
         assertExtentEquals(new long[] {370,  40,  4},
-                           new long[] {389, 339, 10}, grid.getExtent(envelope));
+                           new long[] {389, 339, 10}, grid.subExtent(envelope));
     }
 
     /**
-     * Tests {@link GridGeometry#getExtent(Envelope)} with a non-linear "grid to CRS" transform.
+     * Tests {@link GridGeometry#subExtent(Envelope)} with a non-linear "grid to CRS" transform.
      *
      * @throws TransformException if an error occurred while using the "grid to CRS" transform.
      */
     @Test
-    @DependsOnMethod({"testNonLinear", "testGetExtent"})
-    public void testGetExtentNonLinear() throws TransformException {
+    @DependsOnMethod({"testNonLinear", "testSubExtent"})
+    public void testSubExtentNonLinear() throws TransformException {
         final GridExtent extent = new GridExtent(
                 new DimensionNameType[] {
                     DimensionNameType.COLUMN,
@@ -304,7 +304,7 @@ public final strictfp class GridGeometryTest extends TestCase {
         final MathTransform gridToCRS = MathTransforms.concatenate(linear, MathTransforms.passThrough(1, latitude, 1));
         final GridGeometry  grid      = new GridGeometry(extent, PixelInCell.CELL_CENTER, gridToCRS, HardCodedCRS.WGS84_3D);
         /*
-         * Following tests is similar to the one executed in testGetExtent(). Expected values are only
+         * Following tests is similar to the one executed in testSubExtent(). Expected values are only
          * anti-regression values, except the vertical range which is expected to cover all cells. The
          * main purpose of this test is to verify that TransformSeparator has been able to extract the
          * two-dimensional transform despite its non-linear component.
@@ -312,7 +312,7 @@ public final strictfp class GridGeometryTest extends TestCase {
         final GeneralEnvelope envelope = new GeneralEnvelope(HardCodedCRS.WGS84);
         envelope.setRange(0, -70.001, +80.002);
         envelope.setRange(1,  -4.997,  15.003);
-        final GridExtent actual = grid.getExtent(envelope);
+        final GridExtent actual = grid.subExtent(envelope);
         assertEquals(extent.getAxisType(0), actual.getAxisType(0));
         assertExtentEquals(new long[] { 56, 69, 2},
                            new long[] {130, 73, 4}, actual);
@@ -324,7 +324,7 @@ public final strictfp class GridGeometryTest extends TestCase {
      * @throws TransformException if an error occurred during computation.
      */
     @Test
-    @DependsOnMethod({"testFromGeospatialEnvelope", "testGetExtent"})
+    @DependsOnMethod({"testFromGeospatialEnvelope", "testSubExtent"})
     public void testSubgrid() throws TransformException {
         final GeneralEnvelope envelope = new GeneralEnvelope(HardCodedCRS.WGS84_φλ);
         envelope.setRange(0, -70, +80);
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.java
index bb81dae..cf0e8bb 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.java
@@ -113,6 +113,11 @@ public final class Resources extends IndexedResourceBundle {
         public static final short CanNotParseCombinedReference_2 = 78;
 
         /**
+         * Can not separate the “{0}” coordinate reference system into sub-components.
+         */
+        public static final short CanNotSeparateCRS_1 = 84;
+
+        /**
          * Target dimension {0} depends on excluded source dimensions.
          */
         public static final short CanNotSeparateTargetDimension_1 = 7;
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.properties b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.properties
index 35b4be2..0e26dad 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.properties
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.properties
@@ -51,6 +51,7 @@ CanNotInferGridSizeFromValues_1   = Can not infer a grid size from the given val
 CanNotInstantiateGeodeticObject_1 = Can not instantiate geodetic object for \u201c{0}\u201d.
 CanNotMapAxisToDirection_1        = Can not map an axis from the specified coordinate system to the \u201c{0}\u201d direction.
 CanNotParseCombinedReference_2    = Can not parse component {1} in the combined {0,choice,0#URN|1#URL}.
+CanNotSeparateCRS_1               = Can not separate the \u201c{0}\u201d coordinate reference system into sub-components.
 CanNotSeparateTransform_3         = Can not separate the transform because result would have {2} {0,choice,0#source|1#target} dimension{2,choice,1#|2#s} instead of {1}.
 CanNotSeparateTargetDimension_1   = Target dimension {0} depends on excluded source dimensions.
 CanNotTransformEnvelopeToGeodetic = Can not transform envelope to a geodetic reference system.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources_fr.properties b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources_fr.properties
index 1181bcc..3e25983 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources_fr.properties
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources_fr.properties
@@ -55,6 +55,7 @@ CanNotFindCommonCRS               = Ne peut pas trouver un syst\u00e8me de r\u00
 CanNotInferGridSizeFromValues_1   = Ne peut pas inf\u00e9rer une taille de grille \u00e0 partir des valeurs donn\u00e9es dans la plage {0}.
 CanNotInstantiateGeodeticObject_1 = Ne peut pas cr\u00e9er l\u2019objet g\u00e9od\u00e9tique pour \u00ab\u202f{0}\u202f\u00bb.
 CanNotMapAxisToDirection_1        = Aucun axe du syst\u00e8me de coordonn\u00e9es sp\u00e9cifi\u00e9 n\u2019a pu \u00eatre associ\u00e9 \u00e0 la direction \u00ab\u202f{0}\u202f\u00bb.
+CanNotSeparateCRS_1               = Ne peut pas s\u00e9parer le syst\u00e8me de r\u00e9f\u00e9rence \u00ab\u202f{0}\u202f\u00bb en sous-composantes.
 CanNotSeparateTransform_3         = Ne peut pas s\u00e9parer la transformation parce-que le r\u00e9sultat aurait {2} dimension{2,choice,1#|2#s} en {0,choice,0#entr\u00e9|1#sortie} au lieu de {1}.
 CanNotSeparateTargetDimension_1   = La dimension de destination {0} d\u00e9pend de dimensions sources qui ont \u00e9t\u00e9 exclues.
 CanNotParseCombinedReference_2    = Ne peut pas d\u00e9coder la composante {1} dans l\u2019{0,choice,0#URN|1#URL} combin\u00e9.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/CRS.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/CRS.java
index 62ea42e..8218765 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/CRS.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/CRS.java
@@ -43,6 +43,8 @@ import org.opengis.referencing.crs.ProjectedCRS;
 import org.opengis.referencing.crs.TemporalCRS;
 import org.opengis.referencing.crs.VerticalCRS;
 import org.opengis.referencing.crs.EngineeringCRS;
+import org.opengis.referencing.datum.Datum;
+import org.opengis.referencing.datum.GeodeticDatum;
 import org.opengis.referencing.operation.Conversion;
 import org.opengis.referencing.operation.CoordinateOperationFactory;
 import org.opengis.referencing.operation.OperationNotFoundException;
@@ -62,9 +64,11 @@ import org.apache.sis.internal.referencing.PositionalAccuracyConstant;
 import org.apache.sis.internal.referencing.CoordinateOperations;
 import org.apache.sis.internal.referencing.ReferencingUtilities;
 import org.apache.sis.internal.referencing.DefinitionVerifier;
+import org.apache.sis.internal.referencing.Resources;
 import org.apache.sis.internal.system.DefaultFactories;
 import org.apache.sis.internal.system.Modules;
 import org.apache.sis.internal.system.Loggers;
+import org.apache.sis.internal.util.Numerics;
 import org.apache.sis.referencing.cs.AxisFilter;
 import org.apache.sis.referencing.cs.CoordinateSystems;
 import org.apache.sis.referencing.cs.DefaultVerticalCS;
@@ -1259,6 +1263,7 @@ public final class CRS extends Static {
         ArgumentChecks.ensureValidIndexRange(dimension, lower, upper);
 check:  while (lower != 0 || upper != dimension) {
             if (crs instanceof CompoundCRS) {
+                // We need nested CompoundCRS (if any) below, not a flattened list of SingleCRS.
                 final List<CoordinateReferenceSystem> components = ((CompoundCRS) crs).getComponents();
                 final int size = components.size();
                 for (int i=0; i<size; i++) {
@@ -1282,6 +1287,90 @@ check:  while (lower != 0 || upper != dimension) {
     }
 
     /**
+     * Gets or creates a coordinate reference system with a subset of the dimensions of the given CRS.
+     * This method can be used for dimensionality reduction.
+     *
+     * @param  crs         the CRS to reduce the dimensionality, or {@code null} if none.
+     * @param  dimensions  the dimensions to retain. The dimensions will be taken in increasing order, ignoring duplicated values.
+     * @return a coordinate reference system for the given dimensions. May be the given {@code crs}, which may be {@code null}.
+     * @throws IllegalArgumentException if the given array is empty or if the array contains invalid indices.
+     * @throws FactoryException if the geodetic factory failed to create a compound CRS.
+     *
+     * @since 1.0
+     */
+    public static CoordinateReferenceSystem reduce(final CoordinateReferenceSystem crs, final int... dimensions) throws FactoryException {
+        ArgumentChecks.ensureNonNull("dimensions", dimensions);
+        if (crs == null) {
+            return null;
+        }
+        final int dimension = ReferencingUtilities.getDimension(crs);
+        if (dimension > Long.SIZE) {
+            throw new IllegalArgumentException(Errors.format(Errors.Keys.ExcessiveNumberOfDimensions_1, dimension));
+        }
+        long selected = 0;
+        for (final int d : dimensions) {
+            if (d < 0 || d >= dimension) {
+                throw new IndexOutOfBoundsException(Errors.format(Errors.Keys.IndexOutOfBounds_1, d));
+            }
+            selected |= (1L << d);
+        }
+        if (selected == 0) {
+            throw new IllegalArgumentException(Errors.format(Errors.Keys.EmptyArgument_1, "dimensions"));
+        }
+        final List<CoordinateReferenceSystem> components = new ArrayList<>(Long.bitCount(selected));
+        reduce(0, crs, dimension, selected, components);
+        return compound(components.toArray(new CoordinateReferenceSystem[components.size()]));
+    }
+
+    /**
+     * Adds the components of reduced CRS into the given list.
+     * This method may invoke itself recursively for walking through compound CRS.
+     *
+     * @param  previous    number of dimensions of previous CRS.
+     * @param  crs         the CRS for which to select components.
+     * @param  dimension   number of dimensions of {@code crs}.
+     * @param  selected    bitmask of dimensions to select.
+     * @param  addTo       where to add CRS components.
+     * @return new bitmask after removal of dimensions of the components added to {@code addTo}.
+     */
+    private static long reduce(int previous, final CoordinateReferenceSystem crs, int dimension, long selected,
+            final List<CoordinateReferenceSystem> addTo) throws FactoryException
+    {
+        final long current = (Numerics.bitmask(dimension) - 1) << previous;
+        final long intersect = selected & current;
+        if (intersect != 0) {
+            if (intersect == current) {
+                addTo.add(crs);
+                selected &= ~current;
+            } else if (crs instanceof CompoundCRS) {
+                for (final CoordinateReferenceSystem component : ((CompoundCRS) crs).getComponents()) {
+                    dimension = ReferencingUtilities.getDimension(component);
+                    selected = reduce(previous, component, dimension, selected, addTo);
+                    if ((selected & current) == 0) break;           // Stop if it would be useless to continue.
+                    previous += dimension;
+                }
+            } else if (dimension == 3 && crs instanceof SingleCRS) {
+                final Datum datum = ((SingleCRS) crs).getDatum();
+                if (datum instanceof GeodeticDatum) {
+                    final boolean isVertical = Long.bitCount(intersect) == 1;               // Presumed for now, verified later.
+                    final int verticalDimension = Long.numberOfTrailingZeros((isVertical ? intersect : ~intersect) >>> previous);
+                    final CoordinateSystemAxis verticalAxis = crs.getCoordinateSystem().getAxis(verticalDimension);
+                    if (AxisDirections.isVertical(verticalAxis.getDirection())) try {
+                        addTo.add(new EllipsoidalHeightSeparator((GeodeticDatum) datum).separate((SingleCRS) crs, isVertical));
+                        selected &= ~current;
+                    } catch (IllegalArgumentException | ClassCastException e) {
+                        throw new FactoryException(Resources.format(Resources.Keys.CanNotSeparateCRS_1, crs.getName()));
+                    }
+                }
+            }
+        }
+        if ((selected & current) != 0) {
+            throw new FactoryException(Resources.format(Resources.Keys.CanNotSeparateCRS_1, crs.getName()));
+        }
+        return selected;
+    }
+
+    /**
      * Returns the Greenwich longitude of the prime meridian of the given CRS in degrees.
      * If the prime meridian uses an other unit than degrees, then the value will be converted.
      *
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/CommonCRS.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/CommonCRS.java
index 220e7e3..d2f44f1 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/CommonCRS.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/CommonCRS.java
@@ -479,32 +479,41 @@ public enum CommonCRS {
         }
         final Datum datum = single.getDatum();
         if (datum instanceof GeodeticDatum) {
-            /*
-             * First, try to search using only the EPSG code. This approach avoid initializing unneeded
-             * geodetic objects (such initializations are costly if they require connection to the EPSG
-             * database).
-             */
-            int epsg = 0;
-            final Identifier identifier = IdentifiedObjects.getIdentifier(datum, Citations.EPSG);
-            if (identifier != null) {
-                final String code = identifier.getCode();
-                if (code != null) try {
-                    epsg = Integer.parseInt(code);
-                } catch (NumberFormatException e) {
-                    Logging.recoverableException(Logging.getLogger(Modules.REFERENCING), CommonCRS.class, "forDatum", e);
-                }
-            }
-            for (final CommonCRS c : values()) {
-                if ((epsg != 0) ? c.datum == epsg : Utilities.equalsIgnoreMetadata(c.datum(), datum)) {
-                    return c;
-                }
-            }
+            final CommonCRS c = forDatum((GeodeticDatum) datum);
+            if (c != null) return c;
         }
         throw new IllegalArgumentException(Errors.format(
                 Errors.Keys.UnsupportedDatum_1, IdentifiedObjects.getName(datum, null)));
     }
 
     /**
+     * Returns the {@code CommonCRS} enumeration value for the given datum, or {@code null} if none.
+     */
+    static CommonCRS forDatum(final GeodeticDatum datum) {
+        /*
+         * First, try to search using only the EPSG code. This approach avoid initializing unneeded
+         * geodetic objects (such initializations are costly if they require connection to the EPSG
+         * database).
+         */
+        int epsg = 0;
+        final Identifier identifier = IdentifiedObjects.getIdentifier(datum, Citations.EPSG);
+        if (identifier != null) {
+            final String code = identifier.getCode();
+            if (code != null) try {
+                epsg = Integer.parseInt(code);
+            } catch (NumberFormatException e) {
+                Logging.recoverableException(Logging.getLogger(Modules.REFERENCING), CommonCRS.class, "forDatum", e);
+            }
+        }
+        for (final CommonCRS c : values()) {
+            if ((epsg != 0) ? c.datum == epsg : Utilities.equalsIgnoreMetadata(c.datum(), datum)) {
+                return c;
+            }
+        }
+        return null;
+    }
+
+    /**
      * Returns the default two-dimensional normalized geographic CRS.
      * The CRS returned by this method has the following properties:
      *
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/EllipsoidalHeightSeparator.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/EllipsoidalHeightSeparator.java
new file mode 100644
index 0000000..c2d4ded
--- /dev/null
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/EllipsoidalHeightSeparator.java
@@ -0,0 +1,136 @@
+/*
+ * 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;
+
+import java.util.Map;
+import java.util.Collections;
+import org.opengis.util.FactoryException;
+import org.opengis.referencing.cs.VerticalCS;
+import org.opengis.referencing.cs.CartesianCS;
+import org.opengis.referencing.cs.EllipsoidalCS;
+import org.opengis.referencing.crs.CRSFactory;
+import org.opengis.referencing.crs.SingleCRS;
+import org.opengis.referencing.crs.VerticalCRS;
+import org.opengis.referencing.crs.ProjectedCRS;
+import org.opengis.referencing.crs.GeographicCRS;
+import org.opengis.referencing.cs.CoordinateSystem;
+import org.opengis.referencing.cs.CoordinateSystemAxis;
+import org.opengis.referencing.datum.GeodeticDatum;
+import org.opengis.referencing.operation.Conversion;
+import org.apache.sis.internal.system.DefaultFactories;
+import org.apache.sis.internal.metadata.AxisDirections;
+import org.apache.sis.internal.referencing.ReferencingUtilities;
+import org.apache.sis.referencing.cs.CoordinateSystems;
+import org.apache.sis.referencing.cs.AxisFilter;
+import org.apache.sis.util.Utilities;
+
+
+/**
+ * Helper class for separating the ellipsoidal height from the horizontal part of a CRS.
+ * This is the converse of {@link org.apache.sis.internal.metadata.EllipsoidalHeightCombiner}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.0
+ *
+ * @see org.apache.sis.internal.metadata.EllipsoidalHeightCombiner
+ *
+ * @since 1.0
+ * @module
+ */
+final class EllipsoidalHeightSeparator implements AxisFilter {
+    /**
+     * The value of {@link SingleCRS#getDatum()}.
+     */
+    private final GeodeticDatum datum;
+
+    /**
+     * Whether to extract the vertical component ({@code true}) or the horizontal component ({@code false}).
+     */
+    private boolean vertical;
+
+    /**
+     * Creates a new separator for a CRS having the given datum.
+     */
+    EllipsoidalHeightSeparator(final GeodeticDatum datum) {
+        this.datum = datum;
+    }
+
+    /**
+     * Returns {@code true} if the given axis shall be included in the new coordinate system.
+     */
+    @Override
+    public boolean accept(final CoordinateSystemAxis axis) {
+        return AxisDirections.isVertical(axis.getDirection()) == vertical;
+    }
+
+    /**
+     * The factory to use for creating new coordinate reference system.
+     */
+    private static CRSFactory factory() {
+        return DefaultFactories.forBuildin(CRSFactory.class);
+    }
+
+    /**
+     * Returns properties with the name of the given CRS.
+     */
+    private static Map<String,?> properties(final SingleCRS component) {
+        return Collections.singletonMap(SingleCRS.NAME_KEY, component.getName());
+    }
+
+    /**
+     * Extracts the horizontal or vertical component of the coordinate reference system.
+     *
+     * @param  crs       the coordinate reference system from which to extract the horizontal or vertical component.
+     * @param  vertical  whether to extract the vertical component ({@code true}) or the horizontal component ({@code false}).
+     * @return the requested component.
+     * @throws IllegalArgumentException if the specified coordinate system can not be filtered.
+     *         It may be because the coordinate system would contain an illegal number of axes,
+     *         or because an axis would have an unexpected direction or unexpected unit of measurement.
+     * @throws ClassCastException if a coordinate system is not of the expected type.
+     */
+    SingleCRS separate(final SingleCRS crs, final boolean vertical) throws FactoryException {
+        this.vertical = vertical;
+        final CoordinateSystem cs = CoordinateSystems.replaceAxes(crs.getCoordinateSystem(), this);
+        if (vertical) {
+            VerticalCRS component = CommonCRS.Vertical.ELLIPSOIDAL.crs();
+            if (!Utilities.equalsIgnoreMetadata(component.getCoordinateSystem(), cs)) {
+                component = factory().createVerticalCRS(properties(component), component.getDatum(), (VerticalCS) cs);
+            }
+            return component;
+        }
+        if (crs instanceof GeographicCRS) {
+            final CommonCRS ref = CommonCRS.WGS84;
+            if (Utilities.equalsIgnoreMetadata(ref.geographic().getCoordinateSystem(), cs)) {
+                final CommonCRS c = CommonCRS.forDatum(datum);
+                if (c != null) return c.geographic();
+            } else if (Utilities.equalsIgnoreMetadata(ref.normalizedGeographic().getCoordinateSystem(), cs)) {
+                final CommonCRS c = CommonCRS.forDatum(datum);
+                if (c != null) return c.normalizedGeographic();
+            }
+            return factory().createGeographicCRS(properties(crs), datum, (EllipsoidalCS) cs);
+        }
+        if (crs instanceof ProjectedCRS) {
+            GeographicCRS baseCRS = ((ProjectedCRS) crs).getBaseCRS();
+            if (ReferencingUtilities.getDimension(baseCRS) != 2) {
+                baseCRS = (GeographicCRS) separate(baseCRS, false);
+            }
+            Conversion projection = ((ProjectedCRS) crs).getConversionFromBase();
+            return factory().createProjectedCRS(properties(crs), baseCRS, projection, (CartesianCS) cs);
+        }
+        throw new IllegalArgumentException();
+    }
+}
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/AbstractCS.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/AbstractCS.java
index 4115099..20ab633 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/AbstractCS.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/AbstractCS.java
@@ -379,6 +379,8 @@ public class AbstractCS extends AbstractIdentifiedObject implements CoordinateSy
      *
      * @param  axes  the set of axes to give to the new coordinate system.
      * @return a new coordinate system of the same type than {@code this}, but using the given axes.
+     * @throws IllegalArgumentException if {@code axes} contains an unexpected number of axes,
+     *         or if an axis has an unexpected direction or unexpected unit of measurement.
      */
     AbstractCS createForAxes(final Map<String,?> properties, final CoordinateSystemAxis[] axes) {
         return new AbstractCS(properties, axes);
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/CoordinateSystems.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/CoordinateSystems.java
index 03fb76a..b8a00a3 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/CoordinateSystems.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/CoordinateSystems.java
@@ -381,7 +381,9 @@ public final class CoordinateSystems extends Static {
      * @param  filter  the modifications to apply on coordinate system axes.
      * @return the modified coordinate system as a new instance,
      *         or {@code cs} if the given coordinate system was null or does not need any change.
-     * @throws IllegalArgumentException if the specified coordinate system can not be normalized.
+     * @throws IllegalArgumentException if the specified coordinate system can not be filtered.
+     *         It may be because the coordinate system would contain an illegal number of axes,
+     *         or because an axis would have an unexpected direction or unexpected unit of measurement.
      *
      * @see AxesConvention#NORMALIZED
      *
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/DefaultCartesianCS.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/DefaultCartesianCS.java
index f4f4896..7590365 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/DefaultCartesianCS.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/DefaultCartesianCS.java
@@ -236,6 +236,7 @@ public class DefaultCartesianCS extends DefaultAffineCS implements CartesianCS {
     @Override
     final AbstractCS createForAxes(final Map<String,?> properties, final CoordinateSystemAxis[] axes) {
         switch (axes.length) {
+            case 1: return new DefaultVerticalCS(properties, axes);
             case 2: // Fall through
             case 3: return new DefaultCartesianCS(properties, axes);
             default: throw unexpectedDimension(properties, axes, 2);
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/DefaultEllipsoidalCS.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/DefaultEllipsoidalCS.java
index edaa12e..1d77329 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/DefaultEllipsoidalCS.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/DefaultEllipsoidalCS.java
@@ -203,7 +203,7 @@ public class DefaultEllipsoidalCS extends AbstractCS implements EllipsoidalCS {
      */
     private void validateAxes(final Map<String,?> properties) {
         int i = super.getDimension();
-        int n = i - 2; // Number of vertical axes allowed.
+        int n = i - 2;                      // Number of vertical axes allowed.
         while (--i >= 0) {
             final AxisDirection direction = super.getAxis(i).getDirection();
             if (AxisDirections.isVertical(direction) && --n < 0) {
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/CRSTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/CRSTest.java
index c299eed..10c694b 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/CRSTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/CRSTest.java
@@ -26,7 +26,6 @@ import org.opengis.referencing.crs.ProjectedCRS;
 import org.opengis.referencing.crs.GeodeticCRS;
 import org.opengis.referencing.crs.SingleCRS;
 import org.opengis.referencing.cs.CartesianCS;
-import org.apache.sis.referencing.crs.DefaultCompoundCRS;
 import org.apache.sis.referencing.crs.DefaultGeographicCRS;
 import org.apache.sis.referencing.crs.DefaultProjectedCRS;
 import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
@@ -50,7 +49,7 @@ import static org.apache.sis.test.Assert.*;
  * Tests the {@link CRS} class.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.0
  * @since   0.4
  * @module
  */
@@ -323,8 +322,40 @@ public final strictfp class CRSTest extends TestCase {
                 HardCodedCRS.TIME,
                 HardCodedCRS.WGS84,
                 HardCodedCRS.GEOID_3D,
-                new DefaultCompoundCRS(IdentifiedObjects.getProperties(HardCodedCRS.GEOID_4D),
-                        HardCodedCRS.GEOID_3D, HardCodedCRS.TIME));
+                HardCodedCRS.NESTED);
+    }
+
+    /**
+     * Tests {@link CRS#reduce(CoordinateReferenceSystem, int...)} in the simpler case where
+     * there is no three-dimensional geographic CRS to separate.
+     *
+     * @throws FactoryException if an error occurred while creating a compound CRS.
+     *
+     * @since 1.0
+     */
+    @Test
+    public void testReduce() throws FactoryException {
+        assertSame(HardCodedCRS.TIME,                     CRS.reduce(HardCodedCRS.GEOID_4D, 3));
+        assertSame(HardCodedCRS.GRAVITY_RELATED_HEIGHT,   CRS.reduce(HardCodedCRS.GEOID_4D, 2));
+        assertSame(HardCodedCRS.WGS84,                    CRS.reduce(HardCodedCRS.GEOID_4D, 0, 1));
+        assertSame(HardCodedCRS.GEOID_4D,                 CRS.reduce(HardCodedCRS.GEOID_4D, 0, 1, 2, 3));
+        assertSame(HardCodedCRS.NESTED,                   CRS.reduce(HardCodedCRS.NESTED,   0, 1, 2, 3));
+        assertSame(HardCodedCRS.GEOID_3D,                 CRS.reduce(HardCodedCRS.NESTED,   0, 1, 2));
+        assertEqualsIgnoreMetadata(HardCodedCRS.GEOID_3D, CRS.reduce(HardCodedCRS.GEOID_4D, 0, 1, 2));
+    }
+
+    /**
+     * Tests {@link CRS#reduce(CoordinateReferenceSystem, int...)} with a three-dimensional geographic CRS
+     * to be reduced to a two-dimensional CRS.
+     *
+     * @throws FactoryException if an error occurred while creating a CRS.
+     *
+     * @since 1.0
+     */
+    @Test
+    public void testReduceGeographic3D() throws FactoryException {
+        assertSame(CommonCRS.Vertical.ELLIPSOIDAL.crs(),   CRS.reduce(HardCodedCRS.WGS84_3D, 2));
+        assertSame(CommonCRS.WGS84.normalizedGeographic(), CRS.reduce(HardCodedCRS.WGS84_3D, 0, 1));
     }
 
     /**
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/crs/HardCodedCRS.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/crs/HardCodedCRS.java
index 47134b2..28aa9b2 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/crs/HardCodedCRS.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/crs/HardCodedCRS.java
@@ -35,7 +35,7 @@ import static org.apache.sis.referencing.IdentifiedObjects.getProperties;
  * Collection of coordinate reference systems for testing purpose.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.7
+ * @version 1.0
  * @since   0.4
  * @module
  */
@@ -237,6 +237,12 @@ public final strictfp class HardCodedCRS {
             properties("WGS 84 + height + time", null), WGS84, GRAVITY_RELATED_HEIGHT, TIME);
 
     /**
+     * A (λ,φ,H,t) CRS as a nested compound CRS.
+     */
+    public static final DefaultCompoundCRS NESTED = new DefaultCompoundCRS(
+            properties("(WGS 84 + height) + time", null), GEOID_3D, TIME);
+
+    /**
      * A two-dimensional Cartesian coordinate reference system with (column, row) axes.
      * By default, this CRS has no transformation path to any other CRS (i.e. a map using
      * this CS can't be reprojected to a geographic coordinate reference system for example).


Mime
View raw message