sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] branch geoapi-4.0 updated: Initial commit of a GridGeometry(..., MathTransform, Envelope) constructor.
Date Mon, 24 Sep 2018 02:54:22 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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 151ed6c  Initial commit of a GridGeometry(..., MathTransform, Envelope) constructor.
151ed6c is described below

commit 151ed6cb115ec1c98166dc678ccbd10939814bf8
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Sun Sep 23 22:53:53 2018 -0400

    Initial commit of a GridGeometry(..., MathTransform, Envelope) constructor.
---
 .../sis/internal/metadata/AxisDirections.java      |  41 ++-----
 .../org/apache/sis/coverage/grid/GridExtent.java   | 102 ++++++++++++++++
 .../org/apache/sis/coverage/grid/GridGeometry.java | 135 ++++++++++++++++++---
 .../apache/sis/coverage/grid/GridExtentTest.java   |  80 ++++++++++++
 .../apache/sis/coverage/grid/GridGeometryTest.java |  21 ++++
 .../org/apache/sis/test/suite/RasterTestSuite.java |   1 +
 .../java/org/apache/sis/geometry/Envelopes.java    |   1 +
 .../sis/internal/metadata/AxisDirectionsTest.java  |  10 +-
 8 files changed, 344 insertions(+), 47 deletions(-)

diff --git a/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/AxisDirections.java
b/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/AxisDirections.java
index 3690b19..d1e2be1 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/AxisDirections.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/AxisDirections.java
@@ -157,34 +157,19 @@ public final class AxisDirections extends Static {
      * ({@code NORTH}, {@code EAST}, {@code UP}, {@code FUTURE}).
      * More specifically, the following conversion table is applied:
      *
-     * <table class="compact">
+     * <table class="sis">
      * <caption>Mapping to "absolute" directions</caption><tr>
-     * <td><table class="sis" summary="Geospatial directions">
-     *   <tr>
-     *     <th style="width: 50%">Direction</th>
-     *     <th style="width: 50%">Absolute value</th>
-     *   </tr>
-     *   <tr><td>{@code NORTH}</td> <td>{@code NORTH}</td></tr>
-     *   <tr><td>{@code SOUTH}</td> <td>{@code NORTH}</td></tr>
-     *   <tr><td>{@code EAST}</td>  <td>{@code EAST}</td></tr>
-     *   <tr><td>{@code WEST}</td>  <td>{@code EAST}</td></tr>
-     *   <tr><td>{@code UP}</td>    <td>{@code UP}</td></tr>
-     *   <tr><td>{@code DOWN}</td>  <td>{@code UP}</td></tr>
-     * </table></td>
-     * <td><table class="sis" summary="Other directions">
-     *   <tr>
-     *     <th style="width: 50%">Direction</th>
-     *     <th style="width: 50%">Absolute value</th>
-     *   </tr>
-     *   <tr><td>{@code DISPLAY_RIGHT}</td> <td>{@code DISPLAY_RIGHT}</td></tr>
-     *   <tr><td>{@code DISPLAY_LEFT}</td>  <td>{@code DISPLAY_RIGHT}</td></tr>
-     *   <tr><td>{@code DISPLAY_UP}</td>    <td>{@code DISPLAY_UP}</td></tr>
-     *   <tr><td>{@code DISPLAY_DOWN}</td>  <td>{@code DISPLAY_UP}</td></tr>
-     *   <tr><td>{@code FUTURE}</td>        <td>{@code FUTURE}</td></tr>
-     *   <tr><td>{@code PAST}</td>          <td>{@code FUTURE}</td></tr>
-     *   <tr><td>{@code CLOCKWISE}</td>     <td>{@code COUNTERCLOCKWISE}</td></tr>
-     * </table></td></tr>
-     *   <tr align="center"><td>{@code OTHER}</td><td>{@code OTHER}</td></tr>
+     * <tr><th>Directions</th>                                      <th>Absolute
value</th></tr>
+     * <tr><td>{@code NORTH},           {@code SOUTH}</td>            <td>{@code
NORTH}</td></tr>
+     * <tr><td>{@code EAST},            {@code WEST}</td>             <td>{@code
EAST}</td></tr>
+     * <tr><td>{@code UP},              {@code DOWN}</td>             <td>{@code
UP}</td></tr>
+     * <tr><td>{@code FUTURE},          {@code PAST}</td>             <td>{@code
FUTURE}</td></tr>
+     * <tr><td>{@code COLUMN_POSITIVE}, {@code COLUMN_NEGATIVE}</td>  <td>{@code
COLUMN_POSITIVE}</td></tr>
+     * <tr><td>{@code ROW_POSITIVE},    {@code ROW_NEGATIVE}</td>     <td>{@code
ROW_POSITIVE}</td></tr>
+     * <tr><td>{@code DISPLAY_RIGHT},   {@code DISPLAY_LEFT}</td>     <td>{@code
DISPLAY_RIGHT}</td></tr>
+     * <tr><td>{@code DISPLAY_UP},      {@code DISPLAY_DOWN}</td>     <td>{@code
DISPLAY_UP}</td></tr>
+     * <tr><td>{@code CLOCKWISE},       {@code COUNTERCLOCKWISE}</td> <td>{@code
COUNTERCLOCKWISE}</td></tr>
+     * <tr><td>{@code OTHER}</td>                                     <td>{@code
OTHER}</td></tr>
      * </table>
      *
      * @param  dir  the direction for which to return the absolute direction, or {@code null}.
@@ -330,7 +315,7 @@ public final class AxisDirections extends Static {
     }
 
     /**
-     * Returns {@code true} if the given direction is {@code COLUMN_POSITIVE}, {@code COLUMN_NEGATICE},
+     * Returns {@code true} if the given direction is {@code COLUMN_POSITIVE}, {@code COLUMN_NEGATIVE},
      * {@code ROW_POSITIVE} or {@code ROW_NEGATIVE}.
      *
      * @param  dir  the direction to test, or {@code null}.
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 9d972de..aeefa6e 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
@@ -19,14 +19,21 @@ package org.apache.sis.coverage.grid;
 import java.util.Arrays;
 import java.util.Optional;
 import java.io.Serializable;
+import java.util.Map;
+import java.util.HashMap;
 import org.opengis.geometry.DirectPosition;
 import org.opengis.metadata.spatial.DimensionNameType;
+import org.opengis.referencing.cs.AxisDirection;
+import org.opengis.referencing.cs.CoordinateSystem;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.collection.WeakValueHashMap;
+import org.apache.sis.internal.metadata.AxisDirections;
 import org.apache.sis.internal.raster.Resources;
+import org.apache.sis.geometry.AbstractEnvelope;
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.geometry.Envelopes;
 import org.apache.sis.geometry.GeneralDirectPosition;
@@ -66,6 +73,22 @@ public class GridExtent implements Serializable {
     private static final long serialVersionUID = -4717353677844056017L;
 
     /**
+     * The dimension name types for given coordinate system axis directions.
+     * This map contains only the "positive" axis directions.
+     *
+     * @todo Verify if there is more directions to add as of ISO 19111:2018.
+     */
+    private static final Map<AxisDirection,DimensionNameType> AXIS_DIRECTIONS;
+    static {
+        final Map<AxisDirection,DimensionNameType> dir = new HashMap<>(6);
+        dir.put(AxisDirection.COLUMN_POSITIVE, DimensionNameType.COLUMN);
+        dir.put(AxisDirection.ROW_POSITIVE,    DimensionNameType.ROW);
+        dir.put(AxisDirection.UP,              DimensionNameType.VERTICAL);
+        dir.put(AxisDirection.FUTURE,          DimensionNameType.TIME);
+        AXIS_DIRECTIONS = dir;
+    }
+
+    /**
      * Default axis types for the two-dimensional cases.
      */
     private static final DimensionNameType[] DEFAULT_TYPES = new DimensionNameType[] {
@@ -250,11 +273,88 @@ public class GridExtent implements Serializable {
     }
 
     /**
+     * Creates a new grid extent by rounding the given envelope to (usually) nearest integers.
+     * The envelope coordinates shall be cell indices with lower values inclusive and upper
values exclusive.
+     * Envelopes crossing the anti-meridian shall be {@linkplain GeneralEnvelope#simplify()
simplified}.
+     * The envelope CRS is ignored, except for identifying dimension names for information
purpose.
+     * The way floating point values are rounded to integers may be adjusted in any future
version.
+     *
+     * <p><b>NOTE:</b> this constructor is not public because its contract
is a bit approximative.</p>
+     *
+     * @param  envelope  the envelope containing cell indices to store in a {@code GridExtent}.
+     *
+     * @see #toCRS(MathTransform)
+     */
+    GridExtent(final AbstractEnvelope envelope) {
+        final int dimension = envelope.getDimension();
+        ordinates = allocate(dimension);
+        for (int i=0; i<dimension; i++) {
+            final double min = envelope.getLower(i);
+            final double max = envelope.getUpper(i);
+            if (min >= Long.MIN_VALUE && max <= Long.MAX_VALUE && min
<= max) {
+                long lower = Math.round(min);
+                long upper = Math.round(max);
+                if (lower != upper) upper--;                                // For making
the coordinate inclusive.
+                /*
+                 * The [lower … upper] range may be slightly larger than desired in some
rounding error situations.
+                 * For example if 'min' was 1.49999 and 'max' was 2.50001, the roundings
will create a [1…3] range
+                 * while there is actually only 2 pixels. We detect those rounding problems
by comparing the spans
+                 * before and after rounding. We attempt an adjustment only if the span mistmatch
is ±1, otherwise
+                 * the difference is assumed to be caused by overflow. On the three values
that can be affected by
+                 * the adjustment (min, max and span), we change only the number which is
farthest from an integer
+                 * value.
+                 */
+                long error = (upper - lower) + 1;                           // Negative number
if overflow.
+                if (error >= 0) {
+                    final double span = envelope.getSpan(i);
+                    final long extent = Math.round(span);
+                    if (extent != 0 && Math.abs(error -= extent) == 1) {
+                        final double dmin = Math.abs(min - Math.rint(min));
+                        final double dmax = Math.abs(max - Math.rint(max));
+                        final boolean adjustMax = (dmax >= dmin);
+                        if (Math.abs(span - extent) < (adjustMax ? dmax : dmin)) {
+                            if (adjustMax) upper = Math.subtractExact(upper, error);
+                            else lower = Math.addExact(lower, error);
+                        }
+                    }
+                }
+                ordinates[i] = lower;
+                ordinates[i + dimension] = upper;
+            } else {
+                throw new IllegalArgumentException(Resources.format(
+                        Resources.Keys.IllegalGridEnvelope_3, i, min, max));
+            }
+        }
+        /*
+         * At this point we finished to compute ordinate values.
+         * Now try to infer dimension types from the CRS axes.
+         * This is only for information purpose.
+         */
+        DimensionNameType[] axisTypes = null;
+        final CoordinateReferenceSystem crs = envelope.getCoordinateReferenceSystem();
+        if (crs != null) {
+            final CoordinateSystem cs = crs.getCoordinateSystem();
+            for (int i=0; i<dimension; i++) {
+                final DimensionNameType type = AXIS_DIRECTIONS.get(AxisDirections.absolute(cs.getAxis(i).getDirection()));
+                if (type != null) {
+                    if (axisTypes == null) {
+                        axisTypes = new DimensionNameType[dimension];
+                    }
+                    axisTypes[i] = type;
+                }
+            }
+        }
+        types = validateAxisTypes(axisTypes);
+    }
+
+    /**
      * Creates a new grid envelope as a copy of the given one.
      *
      * @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) {
         ArgumentChecks.ensureNonNull("extent", extent);
@@ -421,6 +521,8 @@ public class GridExtent implements Serializable {
      *
      * @param  gridToCRS  a transform from <em>pixel corner</em> to real world
coordinates
      * @return this grid extent in real world coordinates.
+     *
+     * @see #GridExtent(AbstractEnvelope)
      */
     final GeneralEnvelope toCRS(final MathTransform gridToCRS) throws TransformException
{
         final int dimension = getDimension();
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 878f644..47d800f 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
@@ -29,6 +29,7 @@ import org.opengis.referencing.operation.TransformException;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.cs.CoordinateSystem;
 import org.apache.sis.math.MathFunctions;
+import org.apache.sis.geometry.Envelopes;
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.geometry.ImmutableEnvelope;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
@@ -203,7 +204,7 @@ public class GridGeometry implements Serializable {
     /**
      * Creates a new grid geometry from a grid envelope and a mapping from pixel coordinates
to "real world" coordinates.
      * At least one of {@code extent}, {@code gridToCRS} or {@code crs} arguments shall be
non-null.
-     * If {@code gridToCRS} is non-null, than {@code anchor} shall be non-null too with one
of the following values:
+     * If {@code gridToCRS} is non-null, then {@code anchor} shall be non-null too with one
of the following values:
      *
      * <ul>
      *   <li>{@link PixelInCell#CELL_CENTER} if conversions of cell indices by {@code
gridToCRS} give "real world"
@@ -218,9 +219,12 @@ public class GridGeometry implements Serializable {
      * (with pixels that may be tens of kilometres large) is a recurrent problem. We want
to encourage developers
      * to always think about wether their <cite>grid to CRS</cite> transform
is mapping pixel corner or center.</div>
      *
-     * <div class="note"><b>Upcoming API generalization:</b>
+     * <div class="warning"><b>Upcoming API generalization:</b>
      * the {@code extent} type of this method may be changed to {@code GridEnvelope} interface
in a future Apache SIS version.
-     * This is pending <a href="https://github.com/opengeospatial/geoapi/issues/36">GeoAPI
update</a>.</div>
+     * This is pending <a href="https://github.com/opengeospatial/geoapi/issues/36">GeoAPI
update</a>.
+     * In addition, the {@code PixelInCell} code list currently defined in the {@code org.opengis.referencing.datum}
package
+     * may move in another package in a future GeoAPI version because this type is no longer
defined by the ISO 19111 standard
+     * after the 2018 revision.</div>
      *
      * @param  extent     the valid extent of grid coordinates, or {@code null} if unknown.
      * @param  anchor     {@linkplain PixelInCell#CELL_CENTER Cell center} for OGC conventions
or
@@ -249,7 +253,7 @@ public class GridGeometry implements Serializable {
         this.gridToCRS   = PixelTranslation.translate(gridToCRS, anchor, PixelInCell.CELL_CENTER);
         this.cornerToCRS = PixelTranslation.translate(gridToCRS, anchor, PixelInCell.CELL_CORNER);
         GeneralEnvelope env = null;
-        if (extent != null && gridToCRS != null) {
+        if (extent != null && cornerToCRS != null) {
             env = extent.toCRS(cornerToCRS);
             env.setCoordinateReferenceSystem(crs);
         } else if (crs != null) {
@@ -263,9 +267,9 @@ public class GridGeometry implements Serializable {
          * The easiest way to estimate a resolution is then to ask for the derivative at
some
          * arbitrary point. For this constructor, we take the grid center.
          */
-        final Matrix mat = MathTransforms.getMatrix(gridToCRS);
-        if (mat != null) {
-            resolution = resolution(mat, 1);
+        final Matrix matrix = MathTransforms.getMatrix(gridToCRS);
+        if (matrix != null) {
+            resolution = resolution(matrix, 1);
         } else if (extent != null && gridToCRS != null) {
             resolution = resolution(gridToCRS.derivative(extent.getCentroid()), 0);
         } else {
@@ -275,6 +279,71 @@ public class GridGeometry implements Serializable {
     }
 
     /**
+     * Creates a new grid geometry from a geospatial envelope and a mapping from pixel coordinates
to "real world" coordinates.
+     * At least one of {@code gridToCRS} or {@code envelope} arguments shall be non-null.
+     * If {@code gridToCRS} is non-null, then {@code anchor} shall be non-null too with one
of the values documented in the
+     * {@link #GridGeometry(GridExtent, PixelInCell, MathTransform, CoordinateReferenceSystem)
constructor expecting a grid
+     * extent}.
+     *
+     * <p>The given envelope shall encompass all cell surfaces, from the left border
of leftmost pixel to the right border
+     * of the rightmost pixel and similarly along other axes. This constructor tries to store
a geospatial envelope close
+     * to the specified envelope, but there is no guarantees that the envelope returned by
{@link #getEnvelope()} will be
+     * equal to the given envelope. The envelope stored in the new {@code GridGeometry} may
be slightly smaller, larger or
+     * shifted because the floating point values used in geospatial envelope can not always
be mapped to the integer
+     * coordinates used in {@link GridExtent}.
+     * The rules for deciding whether coordinates should be rounded toward nearest integers,
+     * to {@linkplain Math#floor(double) floor} or to {@linkplain Math#ceil(double) ceil}
values
+     * are implementation details and may be adjusted in any Apache SIS versions.</p>
+     *
+     * <p>Because of the uncertainties explained in above paragraph, this constructor
should be used only in last resort,
+     * when the grid envelope is unknown. For determinist results, developers should prefer
the
+     * {@linkplain #GridGeometry(GridExtent, PixelInCell, MathTransform, CoordinateReferenceSystem)
constructor using grid extent}
+     * as much as possible. In particular, this constructor is not suitable for computing
grid geometry of tiles in a tiled image,
+     * because the above-cited uncertainties may result in apparently random black lines
between tiles.</p>
+     *
+     * <div class="warning"><b>Upcoming API change:</b>
+     * The {@code PixelInCell} code list currently defined in the {@code org.opengis.referencing.datum}
package
+     * may move in another package in a future GeoAPI version because this type is no longer
defined by the
+     * ISO 19111 standard after the 2018 revision. This code list may be taken by ISO 19123
in a future revision.</div>
+     *
+     * @param  anchor     {@linkplain PixelInCell#CELL_CENTER Cell center} for OGC conventions
or
+     *                    {@linkplain PixelInCell#CELL_CORNER cell corner} for Java2D/JAI
conventions.
+     * @param  gridToCRS  the mapping from grid coordinates to "real world" coordinates,
or {@code null} if unknown.
+     * @param  envelope   the geospatial envelope, including its coordinate reference system
if available.
+     *                    There is no guarantees that the envelope actually stored in the
{@code GridGeometry}
+     *                    will be equal to this specified envelope.
+     * @throws TransformException if the math transform can not compute the grid envelope
or the resolution.
+     */
+    @SuppressWarnings("null")
+    public GridGeometry(final PixelInCell anchor, final MathTransform gridToCRS, final Envelope
envelope) throws TransformException {
+        if (gridToCRS == null) {
+            ArgumentChecks.ensureNonNull("envelope", envelope);
+        } else if (envelope != null) {
+            ensureDimensionMatches("envelope", gridToCRS.getTargetDimensions(), envelope.getDimension());
+        }
+        this.gridToCRS   = PixelTranslation.translate(gridToCRS, anchor, PixelInCell.CELL_CENTER);
+        this.cornerToCRS = PixelTranslation.translate(gridToCRS, anchor, PixelInCell.CELL_CORNER);
+        Matrix matrix = MathTransforms.getMatrix(gridToCRS);
+        int numToIgnore = 1;
+        if (envelope != null && cornerToCRS != null) {
+            GeneralEnvelope env = Envelopes.transform(cornerToCRS.inverse(), envelope);
+            extent = new GridExtent(env);
+            env = extent.toCRS(cornerToCRS);
+            env.setCoordinateReferenceSystem(envelope.getCoordinateReferenceSystem());
+            this.envelope = new ImmutableEnvelope(env);
+            if (matrix == null) {
+                matrix = gridToCRS.derivative(extent.getCentroid());    // 'gridToCRS' can
not be null if 'cornerToCRS' is non-null.
+                numToIgnore = 0;
+            }
+        } else {
+            this.extent   = null;
+            this.envelope = ImmutableEnvelope.castOrCopy(envelope);
+        }
+        resolution = (matrix != null) ? resolution(matrix, numToIgnore) : null;
+        nonLinears = findNonLinearTargets(gridToCRS);
+    }
+
+    /**
      * Ensures that the given dimension is equals to the expected value. If not, throws an
exception.
      *
      * @param argument  the name of the argument being tested.
@@ -374,6 +443,8 @@ public class GridGeometry implements Serializable {
      * Returns the bounding box of "real world" coordinates for this grid geometry.
      * This envelope is computed from the {@linkplain #getExtent() grid extent}, which is
      * {@linkplain #getGridToCRS(PixelInCell) transformed} to the "real world" coordinate
system.
+     * The returned envelope encompasses all cell surfaces, from the left border of leftmost
pixel
+     * to the right border of the rightmost pixel and similarly along other axes.
      *
      * @return the bounding box in "real world" coordinates (never {@code null}).
      * @throws IncompleteGridGeometryException if this grid geometry has no envelope —
@@ -747,16 +818,40 @@ public class GridGeometry implements Serializable {
             appendLabel(buffer, "Grid size", visible);
             if (dimension == 0) {
                 buffer.append("unspecified");
-            } else for (int i=0; i<dimension; i++) {
-                if (i != 0) buffer.append(" × ");
-                buffer.append(extent.getSize(i));
-            }
-            appendLabel(buffer, "Grid low", visible);
-            if (dimension == 0) {
-                buffer.append("unspecified");
-            } else for (int i=0; i<dimension; i++) {
-                if (i != 0) buffer.append(", ");
-                buffer.append(extent.getLow(i));
+            } else {
+                /*
+                 * Get the string representations of all GridExtent numbers before to write
them.
+                 * We do that for computing their length, in order to apply right alignment.
+                 */
+                final int NUM_PROPERTIES = 3;
+                final int[] columnSizes = new int[dimension];
+                final String[] values = new String[dimension * NUM_PROPERTIES];      // Will
contain (span, low, high) tuples.
+                for (int i=0; i<values.length; i++) {
+                    final long value;
+                    int margin = 0;
+                    final int dim = i / NUM_PROPERTIES;
+                    switch (i % NUM_PROPERTIES) {
+                        case 0: value = extent.getSize(dim); if (i != 0) margin = 1; break;
+                        case 1: value = extent.getLow (dim); break;
+                        case 2: value = extent.getHigh(dim); break;
+                        default: throw new AssertionError(i);
+                    }
+                    final int length = (values[i] = String.valueOf(value)).length() + margin;
+                    if (length > columnSizes[dim]) columnSizes[dim] = length;
+                }
+                for (int t=0; t<NUM_PROPERTIES; t++) {
+                    String separator = ", ";
+                    switch (t) {
+                        case 0: separator = " ×"; break;
+                        case 1: appendLabel(buffer, "Grid low",  visible); break;
+                        case 2: appendLabel(buffer, "Grid high", visible); break;
+                    }
+                    for (int i=0; i<dimension; i++) {
+                        if (i != 0) buffer.append(separator);
+                        final String value = values[i*NUM_PROPERTIES + t];
+                        buffer.append(CharSequences.spaces(columnSizes[i] - value.length())).append(value);
+                    }
+                }
             }
         }
         CoordinateSystem cs = null;
@@ -795,7 +890,11 @@ public class GridGeometry implements Serializable {
                 if (i != 0) buffer.append(" × ");
                 buffer.append((float) resolution[i]);
                 if (cs != null) {
-                    buffer.append(' ').append(cs.getAxis(i).getUnit());
+                    final String unit = String.valueOf(cs.getAxis(i).getUnit());
+                    if (unit.isEmpty() || Character.isLetterOrDigit(unit.codePointAt(0)))
{
+                        buffer.append(' ');
+                    }
+                    buffer.append(unit);
                 }
             }
         }
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
new file mode 100644
index 0000000..38f83c7
--- /dev/null
+++ b/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridExtentTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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 org.opengis.metadata.spatial.DimensionNameType;
+import org.apache.sis.geometry.AbstractEnvelope;
+import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.referencing.crs.HardCodedCRS;
+import org.apache.sis.test.TestCase;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+
+/**
+ * Tests {@link GridExtent}.
+ *
+ * @author  Martin Desruisseaux (IRD, Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+public final strictfp class GridExtentTest extends TestCase {
+    /**
+     * Tests the {@link GridExtent#GridExtent(AbstractEnvelope)} constructor.
+     */
+    @Test
+    public void testCreateFromEnvelope() {
+        final GeneralEnvelope env = new GeneralEnvelope(HardCodedCRS.IMAGE);
+        env.setRange(0, -23.01, 30.107);
+        env.setRange(1,  12.97, 18.071);
+        GridExtent extent = new GridExtent(env);
+        assertExtentEquals(extent, 0, -23, 29);
+        assertExtentEquals(extent, 1,  13, 17);
+        assertEquals(DimensionNameType.COLUMN, extent.getAxisType(0).get());
+        assertEquals(DimensionNameType.ROW,    extent.getAxisType(1).get());
+    }
+
+    /**
+     * Tests the rounding performed by the {@link GridExtent#GridExtent(AbstractEnvelope)}
constructor.
+     */
+    @Test
+    public void testRoundings() {
+        final GeneralEnvelope env = new GeneralEnvelope(6);
+        env.setRange(0, 1.49999, 3.49998);      // Round to [1…3), stored as [1…2].
+        env.setRange(1, 1.50001, 3.49998);      // Round to [2…3), stored as [1…2] (not
[2…2]) because the span is close to 2.
+        env.setRange(2, 1.49998, 3.50001);      // Round to [1…4), stored as [1…2] (not
[1…3]) because the span is close to 2.
+        env.setRange(3, 1.49999, 3.50002);      // Round to [1…4), stored as [2…3] because
the upper part is closer to integer.
+        env.setRange(4, 1.2,     3.8);          // Round to [1…4), stores as [1…3] because
the span is not close enough to integer.
+        GridExtent extent = new GridExtent(env);
+        assertExtentEquals(extent, 0, 1, 2);
+        assertExtentEquals(extent, 1, 1, 2);
+        assertExtentEquals(extent, 2, 1, 2);
+        assertExtentEquals(extent, 3, 2, 3);
+        assertExtentEquals(extent, 4, 1, 3);
+        assertExtentEquals(extent, 5, 0, 0);    // Unitialized envelope values were [0…0].
+    }
+
+    /**
+     * Verifies the low and high values in the specified dimension of the given extent
+     */
+    private static void assertExtentEquals(final GridExtent extent, final int dimension,
final int low, final int high) {
+        assertEquals("low",  low,  extent.getLow (dimension));
+        assertEquals("high", high, extent.getHigh(dimension));
+    }
+}
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 f660f76..1da415f 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
@@ -24,7 +24,9 @@ import org.apache.sis.referencing.operation.matrix.Matrix3;
 import org.apache.sis.referencing.operation.matrix.Matrix4;
 import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.referencing.crs.HardCodedCRS;
 import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.test.DependsOn;
 import org.apache.sis.test.TestCase;
 import org.junit.Test;
 
@@ -39,6 +41,7 @@ import static org.apache.sis.test.ReferencingAssert.*;
  * @since   1.0
  * @module
  */
+@DependsOn(GridExtentTest.class)
 public final strictfp class GridGeometryTest extends TestCase {
     /**
      * Verifies grid extent coordinates.
@@ -187,4 +190,22 @@ public final strictfp class GridGeometryTest extends TestCase {
         assertFalse("isConversionLinear", grid.isConversionLinear(0, 1, 2, 3));
         assertTrue ("isConversionLinear", grid.isConversionLinear(0, 1,    3));
     }
+
+    /**
+     * Tests the construction from a geospatial envelope.
+     *
+     * @throws TransformException if an error occurred while using the "grid to CRS" transform.
+     */
+    @Test
+    public void testFromGeospatialEnvelope() throws TransformException {
+        final GeneralEnvelope envelope = new GeneralEnvelope(HardCodedCRS.WGS84_φλ);
+        envelope.setRange(0, -70.001, +80.002);
+        envelope.setRange(1,   4.997,  15.003);
+        final MathTransform gridToCRS = MathTransforms.linear(Matrices.create(3, 3, new double[]
{
+            0,   0.5, -90,
+            0.5, 0,  -180,
+            0,   0,     1}));
+        final GridGeometry grid = new GridGeometry(PixelInCell.CELL_CENTER, gridToCRS, envelope);
+        // TODO: verify values.
+    }
 }
diff --git a/core/sis-raster/src/test/java/org/apache/sis/test/suite/RasterTestSuite.java
b/core/sis-raster/src/test/java/org/apache/sis/test/suite/RasterTestSuite.java
index 1817827..f580fb0 100644
--- a/core/sis-raster/src/test/java/org/apache/sis/test/suite/RasterTestSuite.java
+++ b/core/sis-raster/src/test/java/org/apache/sis/test/suite/RasterTestSuite.java
@@ -32,6 +32,7 @@ import org.junit.BeforeClass;
 @Suite.SuiteClasses({
     org.apache.sis.image.DefaultIteratorTest.class,
     org.apache.sis.coverage.grid.PixelTranslationTest.class,
+    org.apache.sis.coverage.grid.GridExtentTest.class,
     org.apache.sis.coverage.grid.GridGeometryTest.class
 })
 public final strictfp class RasterTestSuite extends TestSuite {
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/geometry/Envelopes.java b/core/sis-referencing/src/main/java/org/apache/sis/geometry/Envelopes.java
index 2ec3c31..d6688c1 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/geometry/Envelopes.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/geometry/Envelopes.java
@@ -184,6 +184,7 @@ public final class Envelopes extends Static {
      * Transforms an envelope using the given math transform.
      * The transformation is only approximative: the returned envelope may be bigger than
necessary,
      * or smaller than required if the bounding box contains a pole.
+     * The coordinate reference system of the returned envelope will be null.
      *
      * <div class="section">Limitation</div>
      * This method can not handle the case where the envelope contains the North or South
pole,
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/internal/metadata/AxisDirectionsTest.java
b/core/sis-referencing/src/test/java/org/apache/sis/internal/metadata/AxisDirectionsTest.java
index 0ba6061..4902fb4 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/internal/metadata/AxisDirectionsTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/internal/metadata/AxisDirectionsTest.java
@@ -43,7 +43,7 @@ import static org.apache.sis.internal.metadata.AxisDirections.COUNTER_CLOCKWISE;
  * {@code sis-referencing} module because those tests use {@link HardCodedAxes} constants.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.0
  * @since   0.4
  * @module
  */
@@ -65,6 +65,14 @@ public final strictfp class AxisDirectionsTest extends TestCase {
         assertEquals(UP,                AxisDirections.absolute(DOWN));
         assertEquals(FUTURE,            AxisDirections.absolute(FUTURE));
         assertEquals(FUTURE,            AxisDirections.absolute(PAST));
+        assertEquals(COLUMN_POSITIVE,   AxisDirections.absolute(COLUMN_POSITIVE));
+        assertEquals(COLUMN_POSITIVE,   AxisDirections.absolute(COLUMN_NEGATIVE));
+        assertEquals(ROW_POSITIVE,      AxisDirections.absolute(ROW_POSITIVE));
+        assertEquals(ROW_POSITIVE,      AxisDirections.absolute(ROW_NEGATIVE));
+        assertEquals(DISPLAY_RIGHT,     AxisDirections.absolute(DISPLAY_RIGHT));
+        assertEquals(DISPLAY_RIGHT,     AxisDirections.absolute(DISPLAY_LEFT));
+        assertEquals(DISPLAY_UP,        AxisDirections.absolute(DISPLAY_UP));
+        assertEquals(DISPLAY_UP,        AxisDirections.absolute(DISPLAY_DOWN));
         assertEquals(AWAY_FROM,         AxisDirections.absolute(AWAY_FROM));
         assertEquals(COUNTER_CLOCKWISE, AxisDirections.absolute(CLOCKWISE));
         assertEquals(COUNTER_CLOCKWISE, AxisDirections.absolute(COUNTER_CLOCKWISE));


Mime
View raw message